Source code: org/eclipse/jface/action/ActionContributionItem.java
1 /*******************************************************************************
2 * Copyright (c) 2000, 2004 IBM Corporation and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Common Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/cpl-v10.html
7 *
8 * Contributors:
9 * IBM Corporation - initial API and implementation
10 *******************************************************************************/
11 package org.eclipse.jface.action;
12
13 import java.lang.ref.Reference;
14 import java.lang.ref.ReferenceQueue;
15 import java.lang.ref.WeakReference;
16 import java.util.HashMap;
17 import java.util.Iterator;
18 import java.util.Map;
19
20 import org.eclipse.jface.resource.ImageDescriptor;
21 import org.eclipse.jface.util.IPropertyChangeListener;
22 import org.eclipse.jface.util.Policy;
23 import org.eclipse.jface.util.PropertyChangeEvent;
24 import org.eclipse.swt.SWT;
25 import org.eclipse.swt.graphics.GC;
26 import org.eclipse.swt.graphics.Image;
27 import org.eclipse.swt.graphics.Point;
28 import org.eclipse.swt.graphics.Rectangle;
29 import org.eclipse.swt.widgets.Button;
30 import org.eclipse.swt.widgets.Composite;
31 import org.eclipse.swt.widgets.Display;
32 import org.eclipse.swt.widgets.Event;
33 import org.eclipse.swt.widgets.Item;
34 import org.eclipse.swt.widgets.Listener;
35 import org.eclipse.swt.widgets.Menu;
36 import org.eclipse.swt.widgets.MenuItem;
37 import org.eclipse.swt.widgets.ToolBar;
38 import org.eclipse.swt.widgets.ToolItem;
39 import org.eclipse.swt.widgets.Widget;
40
41 /**
42 * A contribution item which delegates to an action.
43 * <p>
44 * This class may be instantiated; it is not intended to be subclassed.
45 * </p>
46 */
47 public class ActionContributionItem extends ContributionItem {
48
49 /**
50 * Mode bit: Show text on tool items, even if an image is present.
51 * If this mode bit is not set, text is only shown on tool items if there is
52 * no image present.
53 */
54 public static int MODE_FORCE_TEXT = 1;
55 /**
56 * This is the lower bound of the continuous range of accelerator values
57 * that should be handled specially on GTK. This is to circumnavigate the
58 * special input mode in some cases.
59 */
60 private static final int LOWER_GTK_ACCEL_BOUND = SWT.MOD1 | SWT.MOD2 | 'A';
61 /**
62 * This is the lower bound of the continuous range of accelerator values
63 * that should be handled specially on GTK. This is to circumnavigate the
64 * special input mode in some cases.
65 */
66 private static final int UPPER_GTK_ACCEL_BOUND = SWT.MOD1 | SWT.MOD2 | 'F';
67
68 /** a string inserted in the middle of text that has been shortened */
69 private static final String ellipsis = "..."; //$NON-NLS-1$
70
71 /**
72 * A weakly referenced cache of image descriptors to image instances. This
73 * is used to hold images in memory for action contribution items, while
74 * they are defined. When the image descriptor becomes weakly referred to,
75 * the corresponding image will be disposed.
76 */
77 private static final class ImageCache {
78
79 /**
80 * This class spoofs a few method calls by passing them through to the
81 * underlying weakly referred object (if available). This allows the
82 * weak reference to be used as a key in a <code>HashMap</code>.
83 *
84 * @since 3.0
85 */
86 private static final class HashableWeakReference extends WeakReference {
87
88 /**
89 * Constructs a new instance of <code>HashableWeakReference</code>.
90 *
91 * @param referent
92 * The object to refer to; may be <code>null</code>.
93 * @param referenceQueue
94 * The reference queue to use; should not be
95 * <code>null</code>.
96 */
97 private HashableWeakReference(final Object referent,
98 final ReferenceQueue referenceQueue) {
99 super(referent, referenceQueue);
100 }
101
102 /**
103 * @see Object#hashCode()
104 */
105 public final int hashCode() {
106 final Object referent = get();
107 if (referent == null) { return super.hashCode(); }
108
109 return referent.hashCode();
110 }
111
112 /**
113 * @see Object#equals(java.lang.Object)
114 */
115 public final boolean equals(Object object) {
116 final Object referent = get();
117 if (referent == null) { return super.equals(object); }
118
119 if (object instanceof HashableWeakReference) {
120 object = ((HashableWeakReference) object).get();
121 }
122
123 return referent.equals(object);
124 }
125 }
126
127 /**
128 * A thread for cleaner up the reference queues as the garbage collector
129 * fills them. It takes a map and a reference queue. When an item
130 * appears in the reference queue, it uses it as a key to remove values
131 * from the map. If the value is an image, then it is disposed. To
132 * shutdown the thread, call <code>stopCleaning()</code>.
133 *
134 * @since 3.0
135 */
136 private static class ReferenceCleanerThread extends Thread {
137
138 /**
139 * The number of reference cleaner threads created.
140 */
141 private static int threads = 0;
142
143 /**
144 * A marker indicating that the reference cleaner thread should
145 * exit. This is enqueued when the thread is told to stop. Any
146 * referenced enqueued after the thread is told to stop will not be
147 * cleaned up.
148 */
149 private final WeakReference endMarker;
150
151 /**
152 * The reference queue to check; will not be <code>null</code>.
153 */
154 private final ReferenceQueue referenceQueue;
155
156 /**
157 * The map from which to remove values. This value will not be
158 * <code>null</code>.
159 */
160 private final Map map;
161
162 /**
163 * Constructs a new instance of <code>ReferenceCleanerThread</code>.
164 *
165 * @param referenceQueue
166 * The reference queue to check for garbage; mmmmm....
167 * garbage. This value must not be <code>null</code>.
168 * @param map
169 * The map to check for values; must not be
170 * <code>null</code>. It is expected that the keys are
171 * <code>Reference</code> instances. The values are
172 * expected to be <code>Image</code> objects, but it is
173 * okay if they are not.
174 */
175 private ReferenceCleanerThread(final ReferenceQueue referenceQueue,
176 final Map map) {
177 super("Reference Cleaner - " + ++threads); //$NON-NLS-1$
178
179 if (referenceQueue == null) { throw new NullPointerException(
180 "The reference queue should not be null."); } //$NON-NLS-1$
181
182 if (map == null) { throw new NullPointerException(
183 "The map should not be null."); } //$NON-NLS-1$
184
185 this.endMarker = new WeakReference(referenceQueue, referenceQueue);
186 this.referenceQueue = referenceQueue;
187 this.map = map;
188 }
189
190 /**
191 * Tells this thread to stop trying to clean up. This is usually run
192 * when the cache is shutting down.
193 */
194 private final void stopCleaning() {
195 endMarker.enqueue();
196 }
197
198 /**
199 * Waits for new garbage. When new garbage arriving, it removes it,
200 * clears it, and disposes of any corresponding images.
201 */
202 public final void run() {
203 while (true) {
204 // Get the next reference to dispose.
205 Reference reference = null;
206 try {
207 reference = referenceQueue.remove();
208 } catch (final InterruptedException e) {
209 // Reference will be null.
210 }
211
212 // Check to see if we've been told to stop.
213 if (reference == endMarker) {
214 break;
215 }
216
217 // Remove the image and dispose it.
218 final Object value = map.remove(reference);
219 if (value instanceof Image) {
220 Display.getCurrent().syncExec(new Runnable() {
221
222 public void run() {
223 final Image image = (Image) value;
224 if (!image.isDisposed()) {
225 image.dispose();
226 }
227 }
228 });
229 }
230
231 // Clear the reference.
232 if (reference != null) {
233 reference.clear();
234 }
235 }
236 }
237 }
238
239 /**
240 * The thread responsible for cleaning out greyed images that are no
241 * longer needed.
242 */
243 private final ReferenceCleanerThread greyCleaner;
244
245 /**
246 * A map of image descriptors to the corresponding greyed images. The
247 * image descriptors are actually weak references to image descriptors.
248 * As the weak references become suitable for collection, the
249 * corresponding images (i.e., native resources) will be disposed. This
250 * value may be empty, but it is never <code>null</code>.
251 */
252 private final Map greyMap = new HashMap();
253
254 /**
255 * A queue of references waiting to be garbage collected. This value is
256 * never <code>null</code>. This is the queue for
257 * <code>greyMap</code>.
258 */
259 private final ReferenceQueue greyReferenceQueue = new ReferenceQueue();
260
261 /**
262 * The thread responsible for cleaning out images that are no longer
263 * needed.
264 */
265 private final ReferenceCleanerThread imageCleaner;
266
267 /**
268 * A map of image descriptors to the corresponding loaded images. The
269 * image descriptors are actually weak references to image descriptors.
270 * As the weak references become suitable for collection, the
271 * corresponding images (i.e., native resources) will be disposed. This
272 * value may be empty, but it is never <code>null</code>.
273 */
274 private final Map imageMap = new HashMap();
275
276 /**
277 * A queue of references waiting to be garbage collected. This value is
278 * never <code>null</code>. This is the queue for
279 * <code>imageMap</code>.
280 */
281 private final ReferenceQueue imageReferenceQueue = new ReferenceQueue();
282
283 /**
284 * The image to display when no image is available. This value is
285 * <code>null</code> until it is first used.
286 */
287 private Image missingImage = null;
288
289 /**
290 * Constructs a new instance of <code>ImageCache</code>, and starts a
291 * couple of threads to monitor the reference queues.
292 */
293 private ImageCache() {
294 greyCleaner = new ReferenceCleanerThread(greyReferenceQueue,
295 greyMap);
296 imageCleaner = new ReferenceCleanerThread(imageReferenceQueue,
297 imageMap);
298
299 greyCleaner.start();
300 imageCleaner.start();
301
302 }
303
304 /**
305 * Cleans up all images in the cache. This disposes of all of the
306 * images, and drops references to them. This should only be called when
307 * all of the action contribution items are disappearing.
308 */
309 private final void dispose() {
310 // Clean up the missing image.
311 if ((missingImage != null) && (!missingImage.isDisposed())) {
312 missingImage.dispose();
313 missingImage = null;
314 }
315
316 /*
317 * Stop the image cleaner thread, clear all of the weak references
318 * and dispose of all of the images.
319 */
320 imageCleaner.stopCleaning();
321 final Iterator imageItr = imageMap.entrySet().iterator();
322 while (imageItr.hasNext()) {
323 final Map.Entry entry = (Map.Entry) imageItr.next();
324
325 final WeakReference reference = (WeakReference) entry.getKey();
326 reference.clear();
327
328 final Image image = (Image) entry.getValue();
329 if ((image != null) && (!image.isDisposed())) {
330 image.dispose();
331 }
332 }
333 imageMap.clear();
334
335 /*
336 * Stop the greyed image cleaner thread, clear all of the weak
337 * references and dispose of all of the greyed images.
338 */
339 greyCleaner.stopCleaning();
340 final Iterator greyItr = greyMap.entrySet().iterator();
341 while (greyItr.hasNext()) {
342 final Map.Entry entry = (Map.Entry) greyItr.next();
343
344 final WeakReference reference = (WeakReference) entry.getKey();
345 reference.clear();
346
347 final Image image = (Image) entry.getValue();
348 if ((image != null) && (!image.isDisposed())) {
349 image.dispose();
350 }
351 }
352 greyMap.clear();
353 }
354
355 /**
356 * Returns the greyed image (i.e., disabled) for the given image
357 * descriptor. This caches the result so that future attempts to get the
358 * greyed image for the same descriptor will only access the cache. When
359 * the last reference to the image descriptor is dropped, the image will
360 * be cleaned up. This clean up makes no time guarantees about how long
361 * this will take.
362 *
363 * @param descriptor
364 * The image descriptor for which a greyed image should be
365 * created; may be <code>null</code>.
366 * @return The greyed image, either newly created or from the cache.
367 * This value is <code>null</code> if the parameter passed in
368 * is <code>null</code>.
369 */
370 private final Image getGrayImage(final ImageDescriptor descriptor) {
371 if (descriptor == null) { return null; }
372
373 // Try to load a cached image.
374 final HashableWeakReference key = new HashableWeakReference(
375 descriptor, imageReferenceQueue);
376 final Object value = greyMap.get(key);
377 if (value instanceof Image) {
378 key.clear();
379 return (Image) value;
380 }
381
382 // Try to create a grey image from the regular image.
383 final Image image = getImage(descriptor);
384 if (image != null) {
385 final Image greyImage = new Image(null, image, SWT.IMAGE_GRAY);
386 greyMap.put(key, greyImage);
387 return greyImage;
388 }
389
390 // All attempts have failed.
391 return null;
392 }
393
394 /**
395 * Returns the regular image (i.e., enabled) for the given image
396 * descriptor. This caches the result so that future attempts to get the
397 * image for the same descriptor will only access the cache. When the
398 * last reference to the image descriptor is dropped, the image will be
399 * cleaned up. This clean up makes no time guarantees about how long
400 * this will take.
401 *
402 * @param descriptor
403 * The image descriptor for which an image should be created;
404 * may be <code>null</code>.
405 * @return The image, either newly created or from the cache. This value
406 * is <code>null</code> if the parameter passed in is
407 * <code>null</code>.
408 */
409 private final Image getImage(final ImageDescriptor descriptor) {
410 if (descriptor == null) { return null; }
411
412 // Try to load the cached value.
413 final HashableWeakReference key = new HashableWeakReference(
414 descriptor, imageReferenceQueue);
415 final Object value = imageMap.get(key);
416 if (value instanceof Image) {
417 key.clear();
418 return (Image) value;
419 }
420
421 // Use the descriptor to create the image.
422 final Image image = descriptor.createImage();
423 imageMap.put(key, image);
424 return image;
425 }
426
427 /**
428 * Returns the image to display when no image can be found, or none is
429 * specified. This image is only disposed when the cache is disposed.
430 *
431 * @return The image to display for missing images. This value will
432 * never be <code>null</code>.
433 */
434 private final Image getMissingImage() {
435 if (missingImage == null) {
436 missingImage = getImage(ImageDescriptor
437 .getMissingImageDescriptor());
438 }
439
440 return missingImage;
441 }
442 }
443
444 private static ImageCache globalImageCache;
445
446 private static boolean USE_COLOR_ICONS = true;
447
448 /**
449 * Returns whether color icons should be used in toolbars.
450 *
451 * @return <code>true</code> if color icons should be used in toolbars,
452 * <code>false</code> otherwise
453 */
454 public static boolean getUseColorIconsInToolbars() {
455 return USE_COLOR_ICONS;
456 }
457
458 /**
459 * Sets whether color icons should be used in toolbars.
460 *
461 * @param useColorIcons <code>true</code> if color icons should be used in toolbars,
462 * <code>false</code> otherwise
463 */
464 public static void setUseColorIconsInToolbars(boolean useColorIcons) {
465 USE_COLOR_ICONS = useColorIcons;
466 }
467
468 /**
469 * The presentation mode.
470 */
471 private int mode = 0;
472
473 /**
474 * The action.
475 */
476 private IAction action;
477
478 /**
479 * The listener for changes to the text of the action contributed by an
480 * external source.
481 */
482 private final IPropertyChangeListener actionTextListener = new IPropertyChangeListener() {
483
484 /**
485 * @see IPropertyChangeListener#propertyChange(PropertyChangeEvent)
486 */
487 public void propertyChange(PropertyChangeEvent event) {
488 update(event.getProperty());
489 }
490 };
491
492 /**
493 * Listener for SWT button widget events.
494 */
495 private Listener buttonListener;
496
497 /**
498 * Listener for SWT menu item widget events.
499 */
500 private Listener menuItemListener;
501
502 /**
503 * Listener for action property change notifications.
504 */
505 private final IPropertyChangeListener propertyListener = new IPropertyChangeListener() {
506 public void propertyChange(PropertyChangeEvent event) {
507 actionPropertyChange(event);
508 }
509 };
510
511 /**
512 * Listener for SWT tool item widget events.
513 */
514 private Listener toolItemListener;
515
516 /**
517 * The widget created for this item; <code>null</code>
518 * before creation and after disposal.
519 */
520 private Widget widget = null;
521
522 /**
523 * Creates a new contribution item from the given action. The id of the
524 * action is used as the id of the item.
525 *
526 * @param action
527 * the action
528 */
529 public ActionContributionItem(IAction action) {
530 super(action.getId());
531 this.action = action;
532 }
533
534 /**
535 * Handles a property change event on the action (forwarded by nested listener).
536 */
537 private void actionPropertyChange(final PropertyChangeEvent e) {
538 // This code should be removed. Avoid using free asyncExec
539
540 if (isVisible() && widget != null) {
541 Display display = widget.getDisplay();
542 if (display.getThread() == Thread.currentThread()) {
543 update(e.getProperty());
544 } else {
545 display.asyncExec(new Runnable() {
546 public void run() {
547 update(e.getProperty());
548 }
549 });
550 }
551
552 }
553 }
554
555 /**
556 * Compares this action contribution item with another object.
557 * Two action contribution items are equal if they refer to the identical Action.
558 */
559 public boolean equals(Object o) {
560 if (!(o instanceof ActionContributionItem)) {
561 return false;
562 }
563 return action.equals(((ActionContributionItem) o).action);
564 }
565 /**
566 * The <code>ActionContributionItem</code> implementation of this
567 * <code>IContributionItem</code> method creates an SWT <code>Button</code> for
568 * the action using the action's style. If the action's checked property has
569 * been set, the button is created and primed to the value of the checked
570 * property.
571 */
572 public void fill(Composite parent) {
573 if (widget == null && parent != null) {
574 int flags = SWT.PUSH;
575 if (action != null) {
576 if (action.getStyle() == IAction.AS_CHECK_BOX)
577 flags = SWT.TOGGLE;
578 if (action.getStyle() == IAction.AS_RADIO_BUTTON)
579 flags = SWT.RADIO;
580 }
581
582 Button b = new Button(parent, flags);
583 b.setData(this);
584 b.addListener(SWT.Dispose, getButtonListener());
585 // Don't hook a dispose listener on the parent
586 b.addListener(SWT.Selection, getButtonListener());
587 if (action.getHelpListener() != null)
588 b.addHelpListener(action.getHelpListener());
589 widget = b;
590
591 update(null);
592
593 // Attach some extra listeners.
594 action.addPropertyChangeListener(propertyListener);
595 if (action != null) {
596 String commandId = action.getActionDefinitionId();
597 ExternalActionManager.ICallback callback = ExternalActionManager.getInstance()
598 .getCallback();
599
600 if ((callback != null) && (commandId != null)) {
601 callback.addPropertyChangeListener(commandId, actionTextListener);
602 }
603 }
604 }
605 }
606 /**
607 * The <code>ActionContributionItem</code> implementation of this
608 * <code>IContributionItem</code> method creates an SWT <code>MenuItem</code>
609 * for the action using the action's style. If the action's checked property has
610 * been set, a button is created and primed to the value of the checked
611 * property. If the action's menu creator property has been set, a cascading
612 * submenu is created.
613 */
614 public void fill(Menu parent, int index) {
615 if (widget == null && parent != null) {
616 Menu subMenu = null;
617 int flags = SWT.PUSH;
618 if (action != null) {
619 int style = action.getStyle();
620 if (style == IAction.AS_CHECK_BOX)
621 flags = SWT.CHECK;
622 else if (style == IAction.AS_RADIO_BUTTON)
623 flags = SWT.RADIO;
624 else if (style == IAction.AS_DROP_DOWN_MENU) {
625 IMenuCreator mc = action.getMenuCreator();
626 if (mc != null) {
627 subMenu = mc.getMenu(parent);
628 flags = SWT.CASCADE;
629 }
630 }
631 }
632
633 MenuItem mi = null;
634 if (index >= 0)
635 mi = new MenuItem(parent, flags, index);
636 else
637 mi = new MenuItem(parent, flags);
638 widget = mi;
639
640 mi.setData(this);
641 mi.addListener(SWT.Dispose, getMenuItemListener());
642 mi.addListener(SWT.Selection, getMenuItemListener());
643 if (action.getHelpListener() != null)
644 mi.addHelpListener(action.getHelpListener());
645
646 if (subMenu != null)
647 mi.setMenu(subMenu);
648
649 update(null);
650
651 // Attach some extra listeners.
652 action.addPropertyChangeListener(propertyListener);
653 if (action != null) {
654 String commandId = action.getActionDefinitionId();
655 ExternalActionManager.ICallback callback = ExternalActionManager.getInstance()
656 .getCallback();
657
658 if ((callback != null) && (commandId != null)) {
659 callback.addPropertyChangeListener(commandId, actionTextListener);
660 }
661 }
662 }
663 }
664 /**
665 * The <code>ActionContributionItem</code> implementation of this ,
666 * <code>IContributionItem</code> method creates an SWT <code>ToolItem</code>
667 * for the action using the action's style. If the action's checked property has
668 * been set, a button is created and primed to the value of the checked
669 * property. If the action's menu creator property has been set, a drop-down
670 * tool item is created.
671 */
672 public void fill(ToolBar parent, int index) {
673 if (widget == null && parent != null) {
674 int flags = SWT.PUSH;
675 if (action != null) {
676 int style = action.getStyle();
677 if (style == IAction.AS_CHECK_BOX)
678 flags = SWT.CHECK;
679 else if (style == IAction.AS_RADIO_BUTTON)
680 flags = SWT.RADIO;
681 else if (style == IAction.AS_DROP_DOWN_MENU)
682 flags = SWT.DROP_DOWN;
683 }
684
685 ToolItem ti = null;
686 if (index >= 0)
687 ti = new ToolItem(parent, flags, index);
688 else
689 ti = new ToolItem(parent, flags);
690 ti.setData(this);
691 ti.addListener(SWT.Selection, getToolItemListener());
692 ti.addListener(SWT.Dispose, getToolItemListener());
693
694 widget = ti;
695
696 update(null);
697
698 // Attach some extra listeners.
699 action.addPropertyChangeListener(propertyListener);
700 if (action != null) {
701 String commandId = action.getActionDefinitionId();
702 ExternalActionManager.ICallback callback = ExternalActionManager.getInstance()
703 .getCallback();
704
705 if ((callback != null) && (commandId != null)) {
706 callback.addPropertyChangeListener(commandId, actionTextListener);
707 }
708 }
709 }
710 }
711 /**
712 * Returns the action associated with this contribution item.
713 *
714 * @return the action
715 */
716 public IAction getAction() {
717 return action;
718 }
719 /**
720 * Returns the listener for SWT button widget events.
721 *
722 * @return a listener for button events
723 */
724 private Listener getButtonListener() {
725 if (buttonListener == null) {
726 buttonListener = new Listener() {
727 public void handleEvent(Event event) {
728 switch (event.type) {
729 case SWT.Dispose :
730 handleWidgetDispose(event);
731 break;
732 case SWT.Selection :
733 Widget ew = event.widget;
734 if (ew != null) {
735 handleWidgetSelection(event, ((Button) ew).getSelection());
736 }
737 break;
738 }
739 }
740 };
741 }
742 return buttonListener;
743 }
744 /**
745 * Returns the image cache.
746 * The cache is global, and is shared by all action contribution items.
747 * This has the disadvantage that once an image is allocated, it is never freed until the display
748 * is disposed. However, it has the advantage that the same image in different contribution managers
749 * is only ever created once.
750 */
751 private static ImageCache getImageCache() {
752 ImageCache cache = globalImageCache;
753 if (cache == null) {
754 globalImageCache = cache = new ImageCache();
755 Display display = Display.getDefault();
756 if (display != null) {
757 display.disposeExec(new Runnable() {
758 public void run() {
759 if (globalImageCache != null) {
760 globalImageCache.dispose();
761 globalImageCache = null;
762 }
763 }
764 });
765 }
766 }
767 return cache;
768 }
769 /**
770 * Returns the listener for SWT menu item widget events.
771 *
772 * @return a listener for menu item events
773 */
774 private Listener getMenuItemListener() {
775 if (menuItemListener == null) {
776 menuItemListener = new Listener() {
777 public void handleEvent(Event event) {
778 switch (event.type) {
779 case SWT.Dispose :
780 handleWidgetDispose(event);
781 break;
782 case SWT.Selection :
783 Widget ew = event.widget;
784 if (ew != null) {
785 handleWidgetSelection(event, ((MenuItem) ew).getSelection());
786 }
787 break;
788 }
789 }
790 };
791 }
792 return menuItemListener;
793 }
794 /**
795 * Returns the presentation mode, which is the bitwise-or of the
796 * <code>MODE_*</code> constants. The default mode setting is 0, meaning
797 * that for menu items, both text and image are shown (if present), but for
798 * tool items, the text is shown only if there is no image.
799 *
800 * @return the presentation mode settings
801 *
802 * @since 3.0
803 */
804 public int getMode() {
805 return mode;
806 }
807
808 /**
809 * Returns the listener for SWT tool item widget events.
810 *
811 * @return a listener for tool item events
812 */
813 private Listener getToolItemListener() {
814 if (toolItemListener == null) {
815 toolItemListener = new Listener() {
816 public void handleEvent(Event event) {
817 switch (event.type) {
818 case SWT.Dispose :
819 handleWidgetDispose(event);
820 break;
821 case SWT.Selection :
822 Widget ew = event.widget;
823 if (ew != null) {
824 handleWidgetSelection(event, ((ToolItem) ew).getSelection());
825 }
826 break;
827 }
828 }
829 };
830 }
831 return toolItemListener;
832 }
833 /**
834 * Handles a widget dispose event for the widget corresponding to this item.
835 */
836 private void handleWidgetDispose(Event e) {
837 // Check if our widget is the one being disposed.
838 if (e.widget == widget) {
839 // Dispose of the menu creator.
840 if (action.getStyle() == IAction.AS_DROP_DOWN_MENU) {
841 IMenuCreator mc = action.getMenuCreator();
842 if (mc != null) {
843 mc.dispose();
844 }
845 }
846
847 // Unhook all of the listeners.
848 action.removePropertyChangeListener(propertyListener);
849 if (action != null) {
850 String commandId = action.getActionDefinitionId();
851 ExternalActionManager.ICallback callback = ExternalActionManager.getInstance()
852 .getCallback();
853
854 if ((callback != null) && (commandId != null)) {
855 callback.removePropertyChangeListener(commandId, actionTextListener);
856 }
857 }
858
859 // Clear the widget field.
860 widget = null;
861 }
862 }
863 /**
864 * Handles a widget selection event.
865 */
866 private void handleWidgetSelection(Event e, boolean selection) {
867
868 Widget item = e.widget;
869 if (item != null) {
870 int style = item.getStyle();
871
872 if ((style & (SWT.TOGGLE | SWT.CHECK)) != 0) {
873 if (action.getStyle() == IAction.AS_CHECK_BOX) {
874 action.setChecked(selection);
875 }
876 } else if ((style & SWT.RADIO) != 0) {
877 if (action.getStyle() == IAction.AS_RADIO_BUTTON) {
878 action.setChecked(selection);
879 }
880 } else if ((style & SWT.DROP_DOWN) != 0) {
881 if (e.detail == 4) { // on drop-down button
882 if (action.getStyle() == IAction.AS_DROP_DOWN_MENU) {
883 IMenuCreator mc = action.getMenuCreator();
884 ToolItem ti = (ToolItem) item;
885 // we create the menu as a sub-menu of "dummy" so that we can use
886 // it in a cascading menu too.
887 // If created on a SWT control we would get an SWT error...
888 //Menu dummy= new Menu(ti.getParent());
889 //Menu m= mc.getMenu(dummy);
890 //dummy.dispose();
891 if (mc != null) {
892 Menu m = mc.getMenu(ti.getParent());
893 if (m != null) {
894 // position the menu below the drop down item
895 Rectangle b = ti.getBounds();
896 Point p = ti.getParent().toDisplay(new Point(b.x, b.y + b.height));
897 m.setLocation(p.x, p.y); // waiting for SWT 0.42
898 m.setVisible(true);
899 return; // we don't fire the action
900 }
901 }
902 }
903 }
904 }
905
906 // Ensure action is enabled first.
907 // See 1GAN3M6: ITPUI:WINNT - Any IAction in the workbench can be executed while disabled.
908 if (action.isEnabled()) {
909 boolean trace = Policy.TRACE_ACTIONS;
910
911 long ms = System.currentTimeMillis();
912 if (trace)
913 System.out.println("Running action: " + action.getText()); //$NON-NLS-1$
914
915 action.runWithEvent(e);
916
917 if (trace)
918 System.out.println((System.currentTimeMillis() - ms) + " ms to run action: " + action.getText()); //$NON-NLS-1$
919 }
920 }
921 }
922 /* (non-Javadoc)
923 * Method declared on Object.
924 */
925 public int hashCode() {
926 return action.hashCode();
927 }
928
929 /**
930 * Returns whether the given action has any images.
931 *
932 * @param actionToCheck the action
933 * @return <code>true</code> if the action has any images, <code>false</code> if not
934 */
935 private boolean hasImages(IAction actionToCheck) {
936 return actionToCheck.getImageDescriptor() != null
937 || actionToCheck.getHoverImageDescriptor() != null
938 || actionToCheck.getDisabledImageDescriptor() != null;
939 }
940
941 /**
942 * Returns whether the command corresponding to this action
943 * is active.
944 */
945 private boolean isCommandActive() {
946 IAction actionToCheck = getAction();
947
948 if (actionToCheck != null) {
949 String commandId = actionToCheck.getActionDefinitionId();
950 ExternalActionManager.ICallback callback = ExternalActionManager.getInstance().getCallback();
951
952 if (callback != null)
953 return callback.isActive(commandId);
954 }
955 return true;
956 }
957 /**
958 * The action item implementation of this <code>IContributionItem</code>
959 * method returns <code>true</code> for menu items and <code>false</code>
960 * for everything else.
961 */
962 public boolean isDynamic() {
963 if (widget instanceof MenuItem) {
964 //Optimization. Only recreate the item is the check or radio style has changed.
965 boolean itemIsCheck = (widget.getStyle() & SWT.CHECK) != 0;
966 boolean actionIsCheck =
967 getAction() != null && getAction().getStyle() == IAction.AS_CHECK_BOX;
968 boolean itemIsRadio = (widget.getStyle() & SWT.RADIO) != 0;
969 boolean actionIsRadio =
970 getAction() != null && getAction().getStyle() == IAction.AS_RADIO_BUTTON;
971 return (itemIsCheck != actionIsCheck) || (itemIsRadio != actionIsRadio);
972 }
973 return false;
974 }
975
976 /* (non-Javadoc)
977 * Method declared on IContributionItem.
978 */
979 public boolean isEnabled() {
980 return action != null && action.isEnabled();
981 }
982 /**
983 * Returns <code>true</code> if this item is allowed to enable,
984 * <code>false</code> otherwise.
985 *
986 * @return if this item is allowed to be enabled
987 * @since 2.0
988 */
989 protected boolean isEnabledAllowed() {
990 if (getParent() == null)
991 return true;
992 Boolean value = getParent().getOverrides().getEnabled(this);
993 return (value == null) ? true : value.booleanValue();
994 }
995
996 /**
997 * The <code>ActionContributionItem</code> implementation of this
998 * <code>ContributionItem</code> method extends the super implementation
999 * by also checking whether the command corresponding to this action is active.
1000 */
1001 public boolean isVisible() {
1002 return super.isVisible() && isCommandActive();
1003 }
1004
1005 /**
1006 * Sets the presentation mode, which is the bitwise-or of the
1007 * <code>MODE_*</code> constants.
1008 *
1009 * @return the presentation mode settings
1010 *
1011 * @since 3.0
1012 */
1013 public void setMode(int mode) {
1014 this.mode = mode;
1015 update();
1016 }
1017
1018 /**
1019 * The action item implementation of this <code>IContributionItem</code>
1020 * method calls <code>update(null)</code>.
1021 */
1022 public final void update() {
1023 update(null);
1024 }
1025 /**
1026 * Synchronizes the UI with the given property.
1027 *
1028 * @param propertyName the name of the property, or <code>null</code> meaning all applicable
1029 * properties
1030 */
1031 public void update(String propertyName) {
1032 if (widget != null) {
1033 // determine what to do
1034 boolean textChanged = propertyName == null || propertyName.equals(IAction.TEXT);
1035 boolean imageChanged = propertyName == null || propertyName.equals(IAction.IMAGE);
1036 boolean tooltipTextChanged =
1037 propertyName == null || propertyName.equals(IAction.TOOL_TIP_TEXT);
1038 boolean enableStateChanged =
1039 propertyName == null
1040 || propertyName.equals(IAction.ENABLED)
1041 || propertyName.equals(IContributionManagerOverrides.P_ENABLED);
1042 boolean checkChanged =
1043 (action.getStyle() == IAction.AS_CHECK_BOX
1044 || action.getStyle() == IAction.AS_RADIO_BUTTON)
1045 && (propertyName == null || propertyName.equals(IAction.CHECKED));
1046
1047 if (widget instanceof ToolItem) {
1048 ToolItem ti = (ToolItem) widget;
1049 String text = action.getText();
1050 // the set text is shown only if there is no image or if forced by MODE_FORCE_TEXT
1051 boolean showText = text != null && ((getMode() & MODE_FORCE_TEXT) != 0 || !hasImages(action));
1052
1053 // only do the trimming if the text will be used
1054 if (showText && text != null) {
1055 text = Action.removeAcceleratorText(text);
1056 text = Action.removeMnemonics(text);
1057 }
1058
1059 if (textChanged) {
1060 String textToSet = showText ? text : ""; //$NON-NLS-1$
1061 boolean rightStyle = (ti.getParent().getStyle() & SWT.RIGHT) != 0;
1062 if (rightStyle || !ti.getText().equals(textToSet)) {
1063 // In addition to being required to update the text if it
1064 // gets nulled out in the action, this is also a workaround
1065 // for bug 50151: Using SWT.RIGHT on a ToolBar leaves blank space
1066 ti.setText(textToSet);
1067 }
1068 }
1069
1070 if (imageChanged) {
1071 // only substitute a missing image if it has no text
1072 updateImages(!showText);
1073 }
1074
1075 if (tooltipTextChanged || textChanged) {
1076 String toolTip = action.getToolTipText();
1077 // if the text is showing, then only set the tooltip if different
1078 if (!showText || toolTip != null && !toolTip.equals(text)) {
1079 ti.setToolTipText(action.getToolTipText());
1080 }
1081 else {
1082 ti.setToolTipText(null);
1083 }
1084 }
1085
1086 if (enableStateChanged) {
1087 boolean shouldBeEnabled = action.isEnabled() && isEnabledAllowed();
1088
1089 if (ti.getEnabled() != shouldBeEnabled)
1090 ti.setEnabled(shouldBeEnabled);
1091 }
1092
1093 if (checkChanged) {
1094 boolean bv = action.isChecked();
1095
1096 if (ti.getSelection() != bv)
1097 ti.setSelection(bv);
1098 }
1099 return;
1100 }
1101
1102 if (widget instanceof MenuItem) {
1103 MenuItem mi = (MenuItem) widget;
1104
1105 if (textChanged) {
1106 int accelerator = 0;
1107 String acceleratorText = null;
1108 IAction updatedAction = getAction();
1109 String text = null;
1110
1111 // Set the accelerator using the action's accelerator.
1112 accelerator = updatedAction.getAccelerator();
1113
1114 /* Process accelerators on GTK in a special way to avoid
1115 * Bug 42009. We will override the native input method by
1116 * allowing these reserved accelerators to be placed on the
1117 * menu. We will only do this for "Ctrl+Shift+[A-F]".
1118 */
1119 ExternalActionManager.ICallback callback =
1120 ExternalActionManager.getInstance().getCallback();
1121 String commandId = updatedAction.getActionDefinitionId();
1122 if (SWT.getPlatform().equals("gtk")) { //$NON-NLS-1$
1123 if ((callback != null) && (commandId != null)) {
1124 Integer commandAccelerator = callback.getAccelerator(commandId);
1125 if (commandAccelerator != null) {
1126 int accelInt = callback.getAccelerator(commandId).intValue();
1127 if ((accelInt >= LOWER_GTK_ACCEL_BOUND) && (accelInt <= UPPER_GTK_ACCEL_BOUND)) {
1128 accelerator = accelInt;
1129 acceleratorText = callback.getAcceleratorText(commandId);
1130 }
1131 }
1132 }
1133 }
1134
1135 if (accelerator == 0) {
1136 if ((callback != null) && (commandId != null)) {
1137 acceleratorText = callback.getAcceleratorText(commandId);
1138 }
1139 } else {
1140 acceleratorText = Action.convertAccelerator(accelerator);
1141 }
1142
1143 IContributionManagerOverrides overrides = null;
1144
1145 if (getParent() != null)
1146 overrides = getParent().getOverrides();
1147
1148 if (overrides != null)
1149 text = getParent().getOverrides().getText(this);
1150
1151 mi.setAccelerator(accelerator);
1152
1153 if (text == null)
1154 text = updatedAction.getText();
1155
1156 if (text == null)
1157 text = ""; //$NON-NLS-1$
1158 else
1159 text = Action.removeAcceleratorText(text);
1160
1161 if (acceleratorText == null)
1162 mi.setText(text);
1163 else
1164 mi.setText(text + '\t' + acceleratorText);
1165 }
1166
1167 if (imageChanged)
1168 updateImages(false);
1169
1170 if (enableStateChanged) {
1171 boolean shouldBeEnabled = action.isEnabled() && isEnabledAllowed();
1172
1173 if (mi.getEnabled() != shouldBeEnabled)
1174 mi.setEnabled(shouldBeEnabled);
1175 }
1176
1177 if (checkChanged) {
1178 boolean bv = action.isChecked();
1179
1180 if (mi.getSelection() != bv)
1181 mi.setSelection(bv);
1182 }
1183
1184 return;
1185 }
1186
1187 if (widget instanceof Button) {
1188 Button button = (Button) widget;
1189
1190 if (imageChanged && updateImages(false))
1191 textChanged = false; // don't update text if it has an image
1192
1193 if (textChanged) {
1194 String text = action.getText();
1195
1196 if (text != null)
1197 button.setText(text);
1198 }
1199
1200 if (tooltipTextChanged)
1201 button.setToolTipText(action.getToolTipText());
1202
1203 if (enableStateChanged) {
1204 boolean shouldBeEnabled = action.isEnabled() && isEnabledAllowed();
1205
1206 if (button.getEnabled() != shouldBeEnabled)
1207 button.setEnabled(shouldBeEnabled);
1208 }
1209
1210 if (checkChanged) {
1211 boolean bv = action.isChecked();
1212
1213 if (button.getSelection() != bv)
1214 button.setSelection(bv);
1215 }
1216 return;
1217 }
1218 }
1219 }
1220 /**
1221 * Updates the images for this action.
1222 *
1223 * @param forceImage <code>true</code> if some form of image is compulsory,
1224 * and <code>false</code> if it is acceptable for this item to have no image
1225 * @return <code>true</code> if there are images for this action, <code>false</code> if not
1226 */
1227 private boolean updateImages(boolean forceImage) {
1228 ImageCache cache = getImageCache();
1229
1230 if (widget instanceof ToolItem) {
1231 if (USE_COLOR_ICONS) {
1232 Image image = cache.getImage(action.getHoverImageDescriptor());
1233 if (image == null) {
1234 image = cache.getImage(action.getImageDescriptor());
1235 }
1236 Image disabledImage = cache.getImage(action.getDisabledImageDescriptor());
1237
1238 // Make sure there is a valid image.
1239 if (image == null && forceImage) {
1240 image = cache.getMissingImage();
1241 }
1242
1243 // performance: more efficient in SWT to set disabled and hot image before regular image
1244 if (disabledImage != null) {
1245 // Set the disabled image if we were able to create one.
1246 // Assumes that SWT.ToolItem will use platform's default
1247 // behavior to show item when it is disabled and a disabled
1248 // image has not been set.
1249 ((ToolItem) widget).setDisabledImage(disabledImage);
1250 }
1251 ((ToolItem) widget).setImage(image);
1252
1253 return image != null;
1254 } else {
1255 Image image = cache.getImage(action.getImageDescriptor());
1256 Image hoverImage = cache.getImage(action.getHoverImageDescriptor());
1257 Image disabledImage = cache.getImage(action.getDisabledImageDescriptor());
1258
1259 // If there is no regular image, but there is a hover image,
1260 // convert the hover image to gray and use it as the regular image.
1261 if (image == null && hoverImage != null) {
1262 image = cache.getGrayImage(action.getHoverImageDescriptor());
1263 } else {
1264 // If there is no hover image, use the regular image as the hover image,
1265 // and convert the regular image to gray
1266 if (hoverImage == null && image != null) {
1267 hoverImage = image;
1268 image = cache.getGrayImage(action.getImageDescriptor());
1269 }
1270 }
1271
1272 // Make sure there is a valid image.
1273 if (hoverImage == null && image == null && forceImage) {
1274 image = cache.getMissingImage();
1275 }
1276
1277 // performance: more efficient in SWT to set disabled and hot image before regular image
1278 if (disabledImage != null) {
1279 // Set the disabled image if we were able to create one.
1280 // Assumes that SWT.ToolItem will use platform's default
1281 // behavior to show item when it is disabled and a disabled
1282 // image has not been set.
1283 ((ToolItem) widget).setDisabledImage(disabledImage);
1284 }
1285 ((ToolItem) widget).setHotImage(hoverImage);
1286 ((ToolItem) widget).setImage(image);
1287
1288 return image != null;
1289 }
1290 } else if (widget instanceof Item || widget instanceof Button) {
1291 // Use hover image if there is one, otherwise use regular image.
1292 Image image = cache.getImage(action.getHoverImageDescriptor());
1293 if (image == null) {
1294 image = cache.getImage(action.getImageDescriptor());
1295 }
1296 // Make sure there is a valid image.
1297 if (image == null && forceImage) {
1298 image = cache.getMissingImage();
1299 }
1300 if (widget instanceof Item) {
1301 ((Item) widget).setImage(image);
1302 } else if (widget instanceof Button) {
1303 ((Button) widget).setImage(image);
1304 }
1305 return image != null;
1306 }
1307 return false;
1308 }
1309
1310 /**
1311 * Shorten the given text <code>t</code> so that its length doesn't
1312 * exceed the given width. The default implementation replaces characters
1313 * in the center of the original string with an ellipsis ("..."). Override
1314 * if you need a different strategy.
1315 */
1316 protected String shortenText(String textValue , ToolItem item) {
1317 if (textValue == null)
1318 return null;
1319
1320 GC gc = new GC(item.getDisplay());
1321
1322 int maxWidth = item.getImage().getBounds().width * 4;
1323
1324 if(gc.textExtent(textValue).x < maxWidth) {
1325 gc.dispose();
1326 return textValue ;
1327 }
1328
1329 for (int i = textValue.length(); i > 0; i--) {
1330 String test = textValue .substring(0, i);
1331 test = test + ellipsis;
1332 if(gc.textExtent(test).x < maxWidth) {
1333 gc.dispose();
1334 return test ;
1335 }
1336
1337 }
1338 gc.dispose();
1339 //If for some reason we fall through abort
1340 return textValue ;
1341 }
1342}