org.openide.actions.ToolsAction Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.openide.actions;
import java.awt.Component;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import org.openide.awt.Actions;
import org.openide.util.ContextAwareAction;
import org.openide.util.HelpCtx;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.NbBundle;
import org.openide.awt.DynamicMenuContent;
import org.openide.util.LookupListener;
import org.openide.util.RequestProcessor;
import org.openide.util.actions.CookieAction;
import org.openide.util.actions.NodeAction;
import org.openide.util.actions.Presenter;
import org.openide.util.actions.SystemAction;
import org.openide.util.lookup.Lookups;
/** 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.
*
* Note: you do not need to touch this class to add a service action!
* Just register your action into UI/ToolActions
layer folder
* (read more about layers)
* since version 6.15.
*
* @author Jaroslav Tulach
*/
public class ToolsAction extends SystemAction implements ContextAwareAction, Presenter.Menu, Presenter.Popup {
static final long serialVersionUID = 4906417339959070129L;
private static ScheduledFuture taskGl;
// Global ActionManager listener monitoring all available actions
// and their state
static final G gl() {
return gl(Long.MAX_VALUE);
}
static final G gl(long timeOut) {
initGl();
for (;;) {
try {
return taskGl.get(timeOut, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) {
return null;
} catch (InterruptedException ex) {
continue;
} catch (Exception ex) {
taskGl = null;
throw new IllegalStateException(ex);
}
}
}
private static synchronized void initGl() {
if (taskGl == null) {
taskGl = RequestProcessor.getDefault().schedule(new G(), 0, TimeUnit.MILLISECONDS);
}
}
@Override
protected void initialize() {
super.initialize();
}
/* @return name
*/
@Override
public String getName() {
return getActionName();
}
/* @return help for this action
*/
@Override
public HelpCtx getHelpCtx() {
return new HelpCtx(ToolsAction.class);
}
/* @return menu presenter for the action
*/
@Override
public JMenuItem getMenuPresenter() {
return new Inline(this);
}
/* @return menu presenter for the action
*/
@Override
public JMenuItem getPopupPresenter() {
return new Popup(this);
}
/* Does nothing.
*/
@Override
public void actionPerformed(java.awt.event.ActionEvent ev) {
assert false;
}
@Override
public Action createContextAwareInstance(Lookup actionContext) {
return new DelegateAction(this, actionContext);
}
/* @return name
*/
private static String getActionName() {
return NbBundle.getMessage(ToolsAction.class, "CTL_Tools");
}
static List getToolActions() {
ActionManager am = ActionManager.getDefault();
List arr = new ArrayList();
arr.addAll(Arrays.asList(am.getContextActions()));
String pref = arr.isEmpty() ? null : "";
for (Lookup.Item item : gl().result.allItems()) {
final Action action = item.getInstance();
if (action == null) {
continue;
}
String where = item.getId().replaceFirst("[^/]*$", ""); // NOI18N
if (pref != null && !pref.equals(where)) {
arr.add(null);
}
pref = where;
arr.add(action);
}
return arr;
}
/** Implementation method that regenerates the items in the menu or
* in the array.
*
* @param forMenu true if will be presented in menu or false if presented in popup
* @param list (can be null)
*/
private static List generate(Action toolsAction, boolean forMenu) {
List actions = getToolActions();
List list = new ArrayList(actions.size());
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 (Action a : actions) {
// Retrieve context sensitive action instance if possible.
if (lookup != null && a instanceof ContextAwareAction) {
a = ((ContextAwareAction) a).createContextAwareInstance(lookup);
}
if (a == null) {
if (firstItemAdded) {
separator = true;
}
} else {
boolean isPopup = (a instanceof Presenter.Popup);
boolean isMenu = (a instanceof Presenter.Menu);
if (!((forMenu && isMenu) || (!forMenu && isPopup)) && (isMenu || isPopup)) {
continue; // do not call isEnabled on action that is only popup presenter when building menu (i18nPopupAction)
}
if (a.isEnabled()) {
JMenuItem mi;
if (forMenu && isMenu) {
mi = ((Presenter.Menu) a).getMenuPresenter();
} else if (!forMenu && isPopup) {
mi = ((Presenter.Popup) a).getPopupPresenter();
} else if (!isMenu && !isPopup) {
// Generic Swing action.
mi = new JMenuItem();
Actions.connect(mi, a, !forMenu);
} else {
// Should not be here.
continue;
}
if (separator) {
list.add(null);
separator = false;
}
list.add(mi);
firstItemAdded = true;
}
}
}
return list;
}
//------------------------------------------
/** @deprecated Useless, see {@link ActionManager}. */
@Deprecated
public static void setModel(Model m) {
throw new SecurityException();
}
/** @deprecated Useless, see {@link ActionManager}. */
@Deprecated
public static interface Model {
public SystemAction[] getActions();
public void addChangeListener(javax.swing.event.ChangeListener l);
public void removeChangeListener(javax.swing.event.ChangeListener l);
}
/** Inline menu that watches model changes only when really needed.
*/
private static final class Inline extends JMenuItem implements DynamicMenuContent {
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;
}
@NbBundle.Messages({
"LAB_ToolsActionInitializing=Initializing..."
})
@Override
public JComponent[] synchMenuPresenters(JComponent[] items) {
G g = gl(50);
if (g == null) {
JMenuItem init = new JMenuItem();
init.setText(Bundle.LAB_ToolsActionInitializing());
init.setEnabled(false);
return new JMenuItem[] { init };
}
if (timestamp == g.getTimestamp()) {
return items;
}
// generate directly list of menu items
List l = generate(toolsAction, true);
timestamp = gl().getTimestamp();
return l.toArray(new JMenuItem[0]);
}
@Override
public JComponent[] getMenuPresenters() {
return synchMenuPresenters(new JComponent[0]);
}
}
//--------------------------------------------------
/** Inline menu that is either empty or contains one submenu.*/
private static final class Popup extends JMenuItem implements DynamicMenuContent {
static final long serialVersionUID = 2269006599727576059L;
/** sub menu */
private JMenu menu = new MyMenu();
/** Associated tools action. */
private Action toolsAction;
public Popup(Action toolsAction) {
super();
this.toolsAction = toolsAction;
HelpCtx.setHelpIDString(menu, ToolsAction.class.getName());
}
@Override
public JComponent[] synchMenuPresenters(JComponent[] items) {
return gl().isPopupEnabled(toolsAction) ? new JMenuItem[] { menu } : new JMenuItem[0];
}
@Override
public JComponent[] getMenuPresenters() {
return synchMenuPresenters(new JComponent[0]);
}
/** 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());
}
@Override
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);
}
}
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
if (lastPopup != null) {
lastPopup.removePopupMenuListener(this);
lastPopup = null; // clear the status and stop listening
}
}
}
}
//------------------------------------------------
//----------------------------------------------------------
private static class G implements PropertyChangeListener, LookupListener, Callable {
public static final String PROP_STATE = "actionsState"; // NOI18N
private int timestamp = 1;
private Action[] actions = null;
private PropertyChangeSupport supp = new PropertyChangeSupport(this);
Lookup.Result result;
public G() {
// init is done in run
}
@Override
public G call() {
ActionManager am = ActionManager.getDefault();
am.addPropertyChangeListener(this);
result = Lookups.forPath("UI/ToolActions").lookupResult(Action.class); // NOI18N
result.addLookupListener(this);
actionsListChanged();
return this;
}
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
Action[] copy = actions;
if (copy != null) {
for (int i = 0; i < copy.length; i++) {
Action act = copy[i];
if (act != null) {
act.removePropertyChangeListener(this);
}
}
}
ActionManager am = ActionManager.getDefault();
List all = new ArrayList();
all.addAll(Arrays.asList(am.getContextActions()));
all.addAll(result.allInstances());
copy = all.toArray(new Action[0]);
for (int i = 0; i < copy.length; i++) {
Action 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
}
@Override
public void propertyChange(PropertyChangeEvent ev) {
String prop = ev.getPropertyName();
if ((prop == null) || prop.equals(ActionManager.PROP_CONTEXT_ACTIONS)) {
actionsListChanged();
} else if (prop.equals("enabled")) {
actionStateChanged();
}
}
/** Tells if there is any action that is willing to provide
* Presenter.Popup
*/
private boolean isPopupEnabled(Action toolsAction) {
boolean en = false;
Action[] 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++) {
if (copy[i] == null) {
continue;
}
// 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);
if (act == null) {
throw new IllegalStateException("createContextAwareInstance for " + copy[i] + " returned null!");
}
} else {
act = copy[i];
}
if (act.isEnabled()) {
en = true;
break;
}
}
return en;
}
private int getTimestamp() {
return timestamp;
}
@Override
public void resultChanged(LookupEvent ev) {
actionsListChanged();
}
}
/** 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. */
@Override
public String toString() {
return super.toString() + "[delegate=" + delegate + "]"; // NOI18N
}
/** Implements Lookup.Provider
. */
@Override
public Lookup getLookup() {
return lookup;
}
@Override
public void actionPerformed(java.awt.event.ActionEvent e) {
}
@Override
public void putValue(String key, Object o) {
}
@Override
public Object getValue(String key) {
return delegate.getValue(key);
}
@Override
public boolean isEnabled() {
// Irrelevant see G#isPopupEnabled(..).
return delegate.isEnabled();
}
@Override
public void setEnabled(boolean b) {
// Irrelevant see G#isPopupEnabled(..).
}
@Override
public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}
@Override
public void removePropertyChangeListener(PropertyChangeListener listener) {
support.removePropertyChangeListener(listener);
}
/** Implements Presenter.Menu
. */
@Override
public javax.swing.JMenuItem getMenuPresenter() {
return new Inline(this);
}
/** Implements Presenter.Popup
. */
@Override
public javax.swing.JMenuItem getPopupPresenter() {
return new ToolsAction.Popup(this);
}
}
// End of DelegateAction.
}