Source code: org/eclipse/jface/action/MenuManager.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.util.ArrayList;
14 import java.util.Iterator;
15 import java.util.List;
16
17 import org.eclipse.swt.SWT;
18 import org.eclipse.swt.events.MenuAdapter;
19 import org.eclipse.swt.events.MenuEvent;
20 import org.eclipse.swt.widgets.Composite;
21 import org.eclipse.swt.widgets.Control;
22 import org.eclipse.swt.widgets.CoolBar;
23 import org.eclipse.swt.widgets.Decorations;
24 import org.eclipse.swt.widgets.Menu;
25 import org.eclipse.swt.widgets.MenuItem;
26 import org.eclipse.swt.widgets.Shell;
27 import org.eclipse.swt.widgets.ToolBar;
28
29 import org.eclipse.jface.util.ListenerList;
30
31 /**
32 * A menu manager is a contribution manager which realizes itself and its items
33 * in a menu control; either as a menu bar, a sub-menu, or a context menu.
34 * <p>
35 * This class may be instantiated; it may also be subclassed.
36 * </p>
37 */
38 public class MenuManager extends ContributionManager implements IMenuManager {
39
40 /**
41 * The menu id.
42 */
43 private String id;
44
45 /**
46 * List of registered menu listeners (element type: <code>IMenuListener</code>).
47 */
48 private ListenerList listeners = new ListenerList(1);
49
50 /**
51 * The menu control; <code>null</code> before
52 * creation and after disposal.
53 */
54 private Menu menu = null;
55
56 /**
57 * The menu item widget; <code>null</code> before
58 * creation and after disposal. This field is used
59 * when this menu manager is a sub-menu.
60 */
61 private MenuItem menuItem;
62
63 /**
64 * The text for a sub-menu.
65 */
66 private String menuText;
67
68 /**
69 * The overrides for items of this manager
70 */
71 private IContributionManagerOverrides overrides;
72
73 /**
74 * The parent contribution manager.
75 */
76 private IContributionManager parent;
77
78 /**
79 * Indicates whether <code>removeAll</code> should be
80 * called just before the menu is displayed.
81 */
82 private boolean removeAllWhenShown = false;
83
84 /**
85 * Indicates this item is visible in its manager; <code>true</code>
86 * by default.
87 */
88 private boolean visible = true;
89
90 /**
91 * Creates a menu manager. The text and id are <code>null</code>.
92 * Typically used for creating a context menu, where it doesn't need to be referred to by id.
93 */
94 public MenuManager() {
95 this(null, null);
96 }
97
98 /**
99 * Creates a menu manager with the given text. The id of the menu
100 * is <code>null</code>.
101 * Typically used for creating a sub-menu, where it doesn't need to be referred to by id.
102 *
103 * @param text the text for the menu, or <code>null</code> if none
104 */
105 public MenuManager(String text) {
106 this(text, null);
107 }
108
109 /**
110 * Creates a menu manager with the given text and id.
111 * Typically used for creating a sub-menu, where it needs to be referred to by id.
112 *
113 * @param text the text for the menu, or <code>null</code> if none
114 * @param id the menu id, or <code>null</code> if it is to have no id
115 */
116 public MenuManager(String text, String id) {
117 this.menuText = text;
118 this.id = id;
119 }
120
121 /* (non-Javadoc)
122 * @see org.eclipse.jface.action.IMenuManager#addMenuListener(org.eclipse.jface.action.IMenuListener)
123 */
124 public void addMenuListener(IMenuListener listener) {
125 listeners.add(listener);
126 }
127
128 /**
129 * Creates and returns an SWT context menu control for this menu,
130 * and installs all registered contributions.
131 * Does not create a new control if one already exists.
132 * <p>
133 * Note that the menu is not expected to be dynamic.
134 * </p>
135 *
136 * @param parent the parent control
137 * @return the menu control
138 */
139 public Menu createContextMenu(Control parent) {
140 if (!menuExist()) {
141 menu = new Menu(parent);
142 initializeMenu();
143 }
144 return menu;
145 }
146
147 /**
148 * Creates and returns an SWT menu bar control for this menu,
149 * for use in the given <code>Decorations</code>, and installs all registered
150 * contributions. Does not create a new control if one already exists.
151 *
152 * @param parent the parent decorations
153 * @return the menu control
154 * @since 2.1
155 */
156 public Menu createMenuBar(Decorations parent) {
157 if (!menuExist()) {
158 menu = new Menu(parent, SWT.BAR);
159 update(false);
160 }
161 return menu;
162 }
163
164 /**
165 * Creates and returns an SWT menu bar control for this menu, for use in the
166 * given <code>Shell</code>, and installs all registered contributions. Does not
167 * create a new control if one already exists. This implementation simply calls
168 * the <code>createMenuBar(Decorations)</code> method
169 *
170 * @param parent the parent decorations
171 * @return the menu control
172 * @deprecated use <code>createMenuBar(Decorations)</code> instead.
173 */
174 public Menu createMenuBar(Shell parent) {
175 return createMenuBar((Decorations) parent);
176 }
177
178 /**
179 * Disposes of this menu manager and frees all allocated SWT resources.
180 * Notifies all contribution items of the dispose. Note that this method does
181 * not clean up references between this menu manager and its associated
182 * contribution items. Use <code>removeAll</code> for that purpose.
183 */
184 public void dispose() {
185 if (menuExist())
186 menu.dispose();
187 menu = null;
188
189 if (menuItem != null) {
190 menuItem.dispose();
191 menuItem = null;
192 }
193
194 IContributionItem[] items = getItems();
195 for (int i = 0; i < items.length; i++) {
196 items[i].dispose();
197 }
198 }
199
200 /* (non-Javadoc)
201 * @see org.eclipse.jface.action.IContributionItem#fill(org.eclipse.swt.widgets.Composite)
202 */
203 public void fill(Composite parent) {
204 }
205
206 /* (non-Javadoc)
207 * @see org.eclipse.jface.action.IContributionItem#fill(org.eclipse.swt.widgets.CoolBar, int)
208 */
209 public void fill(CoolBar parent, int index) {
210 }
211
212 /* (non-Javadoc)
213 * @see org.eclipse.jface.action.IContributionItem#fill(org.eclipse.swt.widgets.Menu, int)
214 */
215 public void fill(Menu parent, int index) {
216 if (menuItem == null || menuItem.isDisposed()) {
217 if (index >= 0)
218 menuItem = new MenuItem(parent, SWT.CASCADE, index);
219 else
220 menuItem = new MenuItem(parent, SWT.CASCADE);
221
222 menuItem.setText(getMenuText());
223
224 if (!menuExist())
225 menu = new Menu(parent);
226
227 menuItem.setMenu(menu);
228
229 initializeMenu();
230
231 // populate the submenu, in order to enable accelerators
232 // and to set enabled state on the menuItem properly
233 update(true);
234 }
235 }
236
237 /* (non-Javadoc)
238 * @see org.eclipse.jface.action.IContributionItem#fill(org.eclipse.swt.widgets.ToolBar, int)
239 */
240 public void fill(ToolBar parent, int index) {
241 }
242
243 /* (non-Javadoc)
244 * @see org.eclipse.jface.action.IMenuManager#findMenuUsingPath(java.lang.String)
245 */
246 public IMenuManager findMenuUsingPath(String path) {
247 IContributionItem item = findUsingPath(path);
248 if (item instanceof IMenuManager)
249 return (IMenuManager) item;
250 return null;
251 }
252
253 /* (non-Javadoc)
254 * @see org.eclipse.jface.action.IMenuManager#findUsingPath(java.lang.String)
255 */
256 public IContributionItem findUsingPath(String path) {
257 String id = path;
258 String rest = null;
259 int separator = path.indexOf('/');
260 if (separator != -1) {
261 id = path.substring(0, separator);
262 rest = path.substring(separator + 1);
263 } else {
264 return super.find(path);
265 }
266
267 IContributionItem item = super.find(id);
268 if (item instanceof IMenuManager) {
269 IMenuManager manager = (IMenuManager) item;
270 return manager.findUsingPath(rest);
271 }
272 return null;
273 }
274
275 /**
276 * Notifies any menu listeners that a menu is about to show.
277 * Only listeners registered at the time this method is called are notified.
278 *
279 * @param manager the menu manager
280 *
281 * @see IMenuListener#menuAboutToShow
282 */
283 private void fireAboutToShow(IMenuManager manager) {
284 Object[] listeners = this.listeners.getListeners();
285 for (int i = 0; i < listeners.length; ++i) {
286 ((IMenuListener) listeners[i]).menuAboutToShow(manager);
287 }
288 }
289
290 /**
291 * Returns the menu id.
292 * The menu id is used when creating a contribution item
293 * for adding this menu as a sub menu of another.
294 *
295 * @return the menu id
296 */
297 public String getId() {
298 return id;
299 }
300
301 /**
302 * Returns the SWT menu control for this menu manager.
303 *
304 * @return the menu control
305 */
306 public Menu getMenu() {
307 return menu;
308 }
309
310 /**
311 * Returns the text shown in the menu.
312 *
313 * @return the menu text
314 */
315 public String getMenuText() {
316 return menuText;
317 }
318
319 /* (non-Javadoc)
320 * @see org.eclipse.jface.action.IContributionManager#getOverrides()
321 */
322 public IContributionManagerOverrides getOverrides() {
323 if (overrides == null) {
324 if (parent == null) {
325 overrides = new IContributionManagerOverrides() {
326 public Integer getAccelerator(IContributionItem item) {
327 return null;
328 }
329 public String getAcceleratorText(IContributionItem item) {
330 return null;
331 }
332 public Boolean getEnabled(IContributionItem item) {
333 return null;
334 }
335 public String getText(IContributionItem item) {
336 return null;
337 }
338 };
339 } else {
340 overrides = parent.getOverrides();
341 }
342 super.setOverrides(overrides);
343 }
344 return overrides;
345 }
346
347 /**
348 * Returns the parent contribution manager of this manger.
349 *
350 * @return the parent contribution manager
351 * @since 2.0
352 */
353 public IContributionManager getParent() {
354 return parent;
355 }
356
357 /* (non-Javadoc)
358 * @see org.eclipse.jface.action.IMenuManager#getRemoveAllWhenShown()
359 */
360 public boolean getRemoveAllWhenShown() {
361 return removeAllWhenShown;
362 }
363
364 /**
365 * Notifies all listeners that this menu is about to appear.
366 */
367 private void handleAboutToShow() {
368 if (removeAllWhenShown)
369 removeAll();
370 fireAboutToShow(this);
371 update(false, true);
372 }
373
374 /**
375 * Initializes the menu control.
376 */
377 private void initializeMenu() {
378 menu.addMenuListener(new MenuAdapter() {
379 public void menuHidden(MenuEvent e) {
380 // ApplicationWindow.resetDescription(e.widget);
381 }
382 public void menuShown(MenuEvent e) {
383 handleAboutToShow();
384 }
385 });
386 markDirty();
387 // Don't do an update(true) here, in case menu is never opened.
388 // Always do it lazily in handleAboutToShow().
389 }
390
391 /* (non-Javadoc)
392 * @see org.eclipse.jface.action.IContributionItem#isDynamic()
393 */
394 public boolean isDynamic() {
395 return false;
396 }
397
398 /**
399 * Returns whether this menu should be enabled or not.
400 * Used to enable the menu item containing this menu when it is realized as a sub-menu.
401 * <p>
402 * The default implementation of this framework method
403 * returns <code>true</code>. Subclasses may reimplement.
404 * </p>
405 *
406 * @return <code>true</code> if enabled, and
407 * <code>false</code> if disabled
408 */
409 public boolean isEnabled() {
410 return true;
411 }
412
413 /* (non-Javadoc)
414 * @see org.eclipse.jface.action.IContributionItem#isGroupMarker()
415 */
416 public boolean isGroupMarker() {
417 return false;
418 }
419
420 /* (non-Javadoc)
421 * @see org.eclipse.jface.action.IContributionItem#isSeparator()
422 */
423 public boolean isSeparator() {
424 return false;
425 }
426
427 /**
428 * @deprecated this method is no longer a part of the
429 * {@link org.eclipse.jface.action.IContributionItem} API.
430 */
431 public boolean isSubstituteFor(IContributionItem item) {
432 return this.equals(item);
433 }
434
435 /* (non-Javadoc)
436 * @see org.eclipse.jface.action.IContributionItem#isVisible()
437 */
438 public boolean isVisible() {
439 if (!visible)
440 return false; // short circut calculations in this case
441
442 // menus arent visible if all of its children are invisible (or only contains visible separators).
443 IContributionItem [] childItems = getItems();
444 boolean visibleChildren = false;
445 for (int j = 0; j < childItems.length; j++) {
446 if (childItems[j].isVisible() && !childItems[j].isSeparator()) {
447 visibleChildren = true;
448 break;
449 }
450 }
451
452 return visibleChildren;
453 }
454
455 /**
456 * Returns whether the menu control is created
457 * and not disposed.
458 *
459 * @return <code>true</code> if the control is created
460 * and not disposed, <code>false</code> otherwise
461 */
462 private boolean menuExist() {
463 return menu != null && !menu.isDisposed();
464 }
465
466 /* (non-Javadoc)
467 * @see org.eclipse.jface.action.IMenuManager#removeMenuListener(org.eclipse.jface.action.IMenuListener)
468 */
469 public void removeMenuListener(IMenuListener listener) {
470 listeners.remove(listener);
471 }
472
473 /* (non-Javadoc)
474 * @see org.eclipse.jface.action.IContributionItem#saveWidgetState()
475 */
476 public void saveWidgetState() {
477 }
478
479 /**
480 * Sets the overrides for this contribution manager
481 *
482 * @param newOverrides the overrides for the items of this manager
483 * @since 2.0
484 */
485 public void setOverrides(IContributionManagerOverrides newOverrides) {
486 overrides = newOverrides;
487 super.setOverrides(overrides);
488 }
489
490 /* (non-Javadoc)
491 * @see org.eclipse.jface.action.IContributionItem#setParent(org.eclipse.jface.action.IContributionManager)
492 */
493 public void setParent(IContributionManager manager) {
494 parent = manager;
495 }
496
497 /* (non-Javadoc)
498 * @see org.eclipse.jface.action.IMenuManager#setRemoveAllWhenShown(boolean)
499 */
500 public void setRemoveAllWhenShown(boolean removeAll) {
501 this.removeAllWhenShown = removeAll;
502 }
503
504 /* (non-Javadoc)
505 * @see org.eclipse.jface.action.IContributionItem#setVisible(boolean)
506 */
507 public void setVisible(boolean visible) {
508 this.visible = visible;
509 }
510
511 /* (non-Javadoc)
512 * @see org.eclipse.jface.action.IContributionItem#update()
513 */
514 public void update() {
515 updateMenuItem();
516 }
517
518 /**
519 * The <code>MenuManager</code> implementation of this <code>IContributionManager</code>
520 * updates this menu, but not any of its submenus.
521 *
522 * @see #updateAll
523 */
524 public void update(boolean force) {
525 update(force, false);
526 }
527
528 /**
529 * Incrementally builds the menu from the contribution items.
530 * This method leaves out double separators and separators in the first
531 * or last position.
532 *
533 * @param force <code>true</code> means update even if not dirty,
534 * and <code>false</code> for normal incremental updating
535 * @param recursive <code>true</code> means recursively update
536 * all submenus, and <code>false</code> means just this menu
537 */
538 protected void update(boolean force, boolean recursive) {
539 if (isDirty() || force) {
540 if (menuExist()) {
541 // clean contains all active items without double separators
542 IContributionItem[] items = getItems();
543 List clean = new ArrayList(items.length);
544 IContributionItem separator = null;
545 for (int i = 0; i < items.length; ++i) {
546 IContributionItem ci = items[i];
547 if (!ci.isVisible())
548 continue;
549 if (ci.isSeparator()) {
550 // delay creation until necessary
551 // (handles both adjacent separators, and separator at end)
552 separator = ci;
553 } else {
554 if (separator != null) {
555 if (clean.size() > 0) // no separator if first item
556 clean.add(separator);
557 separator = null;
558 }
559 clean.add(ci);
560 }
561 }
562
563 // remove obsolete (removed or non active)
564 MenuItem[] mi = menu.getItems();
565
566 for (int i = 0; i < mi.length; i++) {
567 Object data = mi[i].getData();
568
569 if (data == null || !clean.contains(data)) {
570 mi[i].dispose();
571 } else if (
572 data instanceof IContributionItem
573 && ((IContributionItem) data).isDynamic()
574 && ((IContributionItem) data).isDirty()) {
575 mi[i].dispose();
576 }
577 }
578
579 // add new
580 mi = menu.getItems();
581 int srcIx = 0;
582 int destIx = 0;
583
584 for (Iterator e = clean.iterator(); e.hasNext();) {
585 IContributionItem src = (IContributionItem) e.next();
586 IContributionItem dest;
587
588 // get corresponding item in SWT widget
589 if (srcIx < mi.length)
590 dest = (IContributionItem) mi[srcIx].getData();
591 else
592 dest = null;
593
594 if (dest != null && src.equals(dest)) {
595 srcIx++;
596 destIx++;
597 } else if (dest != null && dest.isSeparator() && src.isSeparator()) {
598 mi[srcIx].setData(src);
599 srcIx++;
600 destIx++;
601 } else {
602 int start = menu.getItemCount();
603 src.fill(menu, destIx);
604 int newItems = menu.getItemCount() - start;
605 for (int i = 0; i < newItems; i++) {
606 MenuItem item = menu.getItem(destIx++);
607 item.setData(src);
608 }
609 }
610
611 // May be we can optimize this call. If the menu has just
612 // been created via the call src.fill(fMenuBar, destIx) then
613 // the menu has already been updated with update(true)
614 // (see MenuManager). So if force is true we do it again. But
615 // we can't set force to false since then information for the
616 // sub sub menus is lost.
617 if (recursive) {
618 IContributionItem item = src;
619 if (item instanceof SubContributionItem)
620 item = ((SubContributionItem) item).getInnerItem();
621 if (item instanceof IMenuManager)
622 ((IMenuManager) item).updateAll(force);
623 }
624
625 }
626
627 // remove any old menu items not accounted for
628 for (; srcIx < mi.length; srcIx++)
629 mi[srcIx].dispose();
630
631 setDirty(false);
632 }
633 } else {
634 // I am not dirty. Check if I must recursivly walk down the hierarchy.
635 if (recursive) {
636 IContributionItem[] items = getItems();
637 for (int i = 0; i < items.length; ++i) {
638 IContributionItem ci = items[i];
639 if (ci instanceof IMenuManager) {
640 IMenuManager mm = (IMenuManager) ci;
641 if (mm.isVisible()) {
642 mm.updateAll(force);
643 }
644 }
645 }
646 }
647 }
648 updateMenuItem();
649 }
650
651 /* (non-Javadoc)
652 * @see org.eclipse.jface.action.IContributionItem#update(java.lang.String)
653 */
654 public void update(String property) {
655 IContributionItem items[] = getItems();
656
657 for (int i = 0; i < items.length; i++)
658 items[i].update(property);
659
660 if (menu != null
661 && !menu.isDisposed()
662 && menu.getParentItem() != null
663 && IAction.TEXT.equals(property)) {
664 String text = getOverrides().getText(this);
665
666 if (text == null)
667 text = getMenuText();
668
669 if (text != null) {
670 ExternalActionManager.ICallback callback =
671 ExternalActionManager.getInstance().getCallback();
672
673 if (callback != null) {
674 int index = text.indexOf('&');
675
676 if (index >= 0 && index < text.length() - 1) {
677 char character = Character.toUpperCase(text.charAt(index + 1));
678
679 if (callback.isAcceleratorInUse(SWT.ALT | character)) {
680 if (index == 0)
681 text = text.substring(1);
682 else
683 text = text.substring(0, index) + text.substring(index + 1);
684 }
685 }
686 }
687
688 menu.getParentItem().setText(text);
689 }
690 }
691 }
692
693 /* (non-Javadoc)
694 * @see org.eclipse.jface.action.IMenuManager#updateAll(boolean)
695 */
696 public void updateAll(boolean force) {
697 update(force, true);
698 }
699
700 /**
701 * Updates the menu item for this sub menu.
702 * The menu item is disabled if this sub menu is empty.
703 * Does nothing if this menu is not a submenu.
704 */
705 private void updateMenuItem() {
706 /*
707 * Commented out until proper solution to enablement of
708 * menu item for a sub-menu is found. See bug 30833 for
709 * more details.
710 *
711 if (menuItem != null && !menuItem.isDisposed() && menuExist()) {
712 IContributionItem items[] = getItems();
713 boolean enabled = false;
714 for (int i = 0; i < items.length; i++) {
715 IContributionItem item = items[i];
716 enabled = item.isEnabled();
717 if(enabled) break;
718 }
719 // Workaround for 1GDDCN2: SWT:Linux - MenuItem.setEnabled() always causes a redraw
720 if (menuItem.getEnabled() != enabled)
721 menuItem.setEnabled(enabled);
722 }
723 */
724 // Partial fix for bug #34969 - diable the menu item if no
725 // items in sub-menu (for context menus).
726 if (menuItem != null && !menuItem.isDisposed() && menuExist()) {
727 boolean enabled = menu.getItemCount() > 0;
728 // Workaround for 1GDDCN2: SWT:Linux - MenuItem.setEnabled() always causes a redraw
729 if (menuItem.getEnabled() != enabled) {
730 // We only do this for context menus (for bug #34969)
731 Menu topMenu = menu;
732 while (topMenu.getParentMenu() != null)
733 topMenu = topMenu.getParentMenu();
734 if ((topMenu.getStyle() & SWT.BAR) == 0)
735 menuItem.setEnabled(enabled);
736 }
737 }
738 }
739 }