/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is NetBeans. The Initial Developer of the Original * Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun * Microsystems, Inc. All Rights Reserved. */ package org.openide.actions; import java.beans.*; import javax.swing.*; import javax.swing.event.*; import java.util.*; import org.openide.actions.ActionManager; import org.openide.awt.JInlineMenu; import org.openide.util.ContextAwareAction; import org.openide.util.NbBundle; import org.openide.util.HelpCtx; import org.openide.util.Lookup; import org.openide.util.actions.*; /** A "meta-action" that displays (in a submenu) a list of enabled actions provided by modules. * Such registered actions are called "service actions": * they are provided externally but seem to provide additional services on existing components. * Often they will be {@link NodeAction}s or {@link CookieAction}s so that they will * be enabled based on the node selection, i.e. the node containing this popup. * It is desirable for most nodes to include this action somewhere in their popup menu. * * <p><em>Note:</em> you do not need to touch this class to add a service action! * Just add the action to a module manifest in an <code>Action</code> section. * * <p>The list of registered service actions is provided to this action from the implementation * by means of {@link ActionManager}. * * @author Jaroslav Tulach */ public class ToolsAction extends SystemAction implements ContextAwareAction, Presenter.Menu, Presenter.Popup { static final long serialVersionUID =4906417339959070129L; // Global ActionManager listener monitoring all available actions // and their state private static G gl; /** Lazy initialization of global listener. */ private static synchronized G gl () { if (gl == null) { gl = new G (); } return gl; } /* @return name */ public String getName () { return getActionName (); } /* @return help for this action */ public HelpCtx getHelpCtx () { return new HelpCtx (ToolsAction.class); } /* @return menu presenter for the action */ public JMenuItem getMenuPresenter () { return new Inline(this); } /* @return menu presenter for the action */ public JMenuItem getPopupPresenter () { return new Popup(this); } /* Does nothing. */ public void actionPerformed (java.awt.event.ActionEvent ev) { } /** Implements <code>ContextAwareAction</code> interface method. */ public Action createContextAwareInstance(Lookup actionContext) { return new DelegateAction(this, actionContext); } /* @return name */ private static String getActionName () { return NbBundle.getMessage(ToolsAction.class, "CTL_Tools"); } /** Implementation method that regenerates the items in the menu or * in the array. * * @param forMenu true if Presenter.Menu should be used false if Presenter.Popup * @param list (can be null) */ private static List generate (Action toolsAction, boolean forMenu) { ActionManager am = (ActionManager)Lookup.getDefault().lookup(ActionManager.class); SystemAction[] actions = am.getContextActions (); List list = new ArrayList( actions.length ); boolean separator = false; boolean firstItemAdded = false; // flag to prevent adding separator before actual menu items // Get action context. Lookup lookup; if(toolsAction instanceof Lookup.Provider) { lookup = ((Lookup.Provider)toolsAction).getLookup(); } else { lookup = null; } for (int i = 0; i < actions.length; i++) { Action a; // Retrieve context sensitive action instance if possible. if(lookup != null && actions[i] instanceof ContextAwareAction) { a = ((ContextAwareAction)actions[i]).createContextAwareInstance(lookup); } else { a = actions[i]; } if (a == null) { if (firstItemAdded) separator = true; } else if( forMenu ) { if( a instanceof Presenter.Menu && a.isEnabled () ) { if (separator) { list.add (null); separator = false; } JMenuItem mi = ((Presenter.Menu)a).getMenuPresenter (); list.add (mi); firstItemAdded = true; } } else if( a instanceof Presenter.Popup && a.isEnabled() ) { if (separator) { list.add (null); separator = false; } JMenuItem mi = ((Presenter.Popup)a).getPopupPresenter (); list.add (mi); firstItemAdded = true; } } return list; } /** Inline menu that watches model changes only when really needed. */ private static final class Inline extends JInlineMenu implements PropertyChangeListener, Runnable { static final long serialVersionUID =2269006599727576059L; /** timestamp of the beginning of the last regeneration */ private int timestamp = 0; /** Associated tools action. */ private Action toolsAction; Inline(Action toolsAction) { this.toolsAction = toolsAction; putClientProperty("hack.preShowUpdater", this); } /** By calling this method, our parent notifies us we've to be keep * updated, so we start listening on SystemAction changes, and * schedule updating Runnable imediately. */ public void addNotify() { // We were not notified by our parent, too bad if(timestamp != gl().getTimestamp()) SwingUtilities.invokeLater(this); gl ().addPropertyChangeListener( this ); super.addNotify(); } /** By calling this method, our parent notifies us we don't have * to be up-to-date more, so we switch to lazy mode and discard any * pending updates. */ public void removeNotify() { gl ().removePropertyChangeListener( this ); super.removeNotify(); } /** Change of model. */ public void propertyChange (PropertyChangeEvent ev) { String prop = ev.getPropertyName (); if (prop == null || prop.equals (G.PROP_STATE)) { SwingUtilities.invokeLater (this); } } /** Runs the update */ public void run () { if( timestamp == gl ().getTimestamp() ) return; // generate directly list of menu items List l = generate (toolsAction, true); setMenuItems ((JMenuItem[])l.toArray(new JMenuItem[l.size ()])); timestamp = gl ().getTimestamp(); } } //-------------------------------------------------- /** Inline menu that is either empty or contains one submenu.*/ private static final class Popup extends JInlineMenu implements Runnable { /** A special menu that will properly update its submenu before posting */ private class MyMenu extends org.openide.awt.JMenuPlus implements PopupMenuListener { /* A popup menu we've attached our listener to. * If null, the content is not up-to-date */ private JPopupMenu lastPopup = null; MyMenu() { super(getActionName()); } public JPopupMenu getPopupMenu() { JPopupMenu popup = super.getPopupMenu(); fillSubmenu(popup); return popup; } private void fillSubmenu(JPopupMenu pop) { if (lastPopup == null) { pop.addPopupMenuListener(this); lastPopup = pop; removeAll (); Iterator it = generate(toolsAction, false).iterator(); while( it.hasNext() ) { java.awt.Component item = (java.awt.Component)it.next(); if( item == null ) { addSeparator (); } else { add ( item ); } } // also work with empty element if(getMenuComponentCount () == 0) { JMenuItem empty = new JMenuItem(NbBundle.getMessage( ToolsAction.class, "CTL_EmptySubMenu")); empty.setEnabled(false); add(empty); } } } public void popupMenuCanceled(PopupMenuEvent e) {} public void popupMenuWillBecomeVisible(PopupMenuEvent e) {} public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { lastPopup.removePopupMenuListener(this); lastPopup = null; // clear the status and stop listening } } static final long serialVersionUID =2269006599727576059L; /** sub menu */ private JMenu menu = new MyMenu(); private boolean inited = false; /** Associated tools action. */ private Action toolsAction; public Popup (Action toolsAction) { super(); this.toolsAction = toolsAction; HelpCtx.setHelpIDString (menu, ToolsAction.class.getName ()); // This can be probably swapped as the popup is short time entity and our hack // will be called just once and very early after the constructor. // run(); putClientProperty("hack.preShowUpdater", this); } public void addNotify() { if (!inited) { // should not happen SwingUtilities.invokeLater (this); // too late to do it here } super.addNotify(); } /** Runs the update */ public void run () { if(!inited) { setMenuItems ( //// gl().isPopupEnabled(toolsAction) //// ? new JMenuItem[] { menu } //// : new JMenuItem[0] // Tools action won't be seen. ); inited = true; } } } //------------------------------------------ /** @deprecated Useless, see {@link ActionManager}. */ public static void setModel (Model m) { throw new SecurityException (); } /** @deprecated Useless, see {@link ActionManager}. */ public static interface Model { public SystemAction[] getActions (); public void addChangeListener (javax.swing.event.ChangeListener l); public void removeChangeListener (javax.swing.event.ChangeListener l); } //------------------------------------------------ //---------------------------------------------------------- private static class G implements PropertyChangeListener { private int timestamp = 1; private SystemAction[] actions = null; private PropertyChangeSupport supp = new PropertyChangeSupport( this ); public static final String PROP_STATE = "actionsState"; // NOI18N public G() { ActionManager am = (ActionManager)Lookup.getDefault().lookup(ActionManager.class); am.addPropertyChangeListener ( this ); actionsListChanged(); } public final void addPropertyChangeListener( PropertyChangeListener listener ) { supp.addPropertyChangeListener(listener); } public final void removePropertyChangeListener( PropertyChangeListener listener ) { supp.removePropertyChangeListener (listener); } protected final void firePropertyChange( String name , Object o, Object n ) { supp.firePropertyChange(name, o, n); } private void actionsListChanged() { timestamp++; // deregister all actions listeners SystemAction[] copy = actions; if( copy != null ) { for( int i = 0; i< copy.length; i++ ) { SystemAction act = copy[i]; if( act != null ) act.removePropertyChangeListener ( this ); } } ActionManager am = (ActionManager)Lookup.getDefault().lookup(ActionManager.class); copy = am.getContextActions (); for (int i = 0; i < copy.length; i++) { SystemAction act = copy[i]; if( act != null ) act.addPropertyChangeListener ( this ); } actions = copy; firePropertyChange( PROP_STATE, null, null ); // tell the world } private void actionStateChanged() { timestamp++; firePropertyChange( PROP_STATE, null, null ); // tell the world } public void propertyChange (PropertyChangeEvent ev) { String prop = ev.getPropertyName (); if (prop == null || prop.equals (ActionManager.PROP_CONTEXT_ACTIONS)) { actionsListChanged(); } else if( prop.equals (SystemAction.PROP_ENABLED)) { actionStateChanged(); } } /** Tells if there is any action that is willing to provide * Presenter.Popup */ private boolean isPopupEnabled(Action toolsAction) { boolean en = false; SystemAction[] copy = actions; // Get action conext. Lookup lookup; if(toolsAction instanceof Lookup.Provider) { lookup = ((Lookup.Provider)toolsAction).getLookup(); } else { lookup = null; } for (int i=0; i<copy.length; i++) { // Get context aware action instance if needed. Action act; // Retrieve context aware action instance if possible. if(lookup != null && copy[i] instanceof ContextAwareAction) { act = ((ContextAwareAction)copy[i]).createContextAwareInstance(lookup); } else { act = copy[i]; } if( act instanceof Presenter.Popup && act.isEnabled() ) { en = true; break; } } return en; } private int getTimestamp() { return timestamp; } } /** Delegate tools action. Which act accordingly to current context * (represented by lookup). */ private static final class DelegateAction extends Object implements Action, Presenter.Menu, Presenter.Popup, Lookup.Provider { private ToolsAction delegate; private Lookup lookup; /** support for listeners */ private PropertyChangeSupport support = new PropertyChangeSupport (this); public DelegateAction(ToolsAction delegate, Lookup actionContext) { this.delegate = delegate; this.lookup = actionContext; } /** Overrides superclass method, adds delegate description. */ public String toString() { return super.toString() + "[delegate=" + delegate + "]"; // NOI18N } /** Implements <code>Lookup.Provider</code>. */ public Lookup getLookup() { return lookup; } public void actionPerformed(java.awt.event.ActionEvent e) { } public void putValue(String key, Object o) {} public Object getValue(String key) { return delegate.getValue(key); } public boolean isEnabled() { // Irrelevant see G#isPopupEnabled(..). return delegate.isEnabled(); } public void setEnabled(boolean b) { // Irrelevant see G#isPopupEnabled(..). } public void addPropertyChangeListener(PropertyChangeListener listener) { support.addPropertyChangeListener (listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { support.removePropertyChangeListener (listener); } /** Implements <code>Presenter.Menu</code>. */ public javax.swing.JMenuItem getMenuPresenter() { return new Inline(this); } /** Implements <code>Presenter.Popup</code>. */ public javax.swing.JMenuItem getPopupPresenter() { return new ToolsAction.Popup(this); } } // End of DelegateAction. }