/*
* 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.util.actions;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.List;
import javax.swing.*;
import org.openide.ErrorManager;
import org.openide.awt.Actions;
import org.openide.awt.JPopupMenuPlus;
import org.openide.util.*;
/* not relevant here --jglick
* The actions system allows connection between action
* "invokers" and an action "performer", where in some cases the
* performer of the action can be the action class itself, while in
* other cases it can be a class that implements the ActionPerformer
* interface and is registered at the action via setActionPerformer.
*/
/**
* The common predecessor of callable actions in the IDE.
* <P>
* Also implements the Swing {@link Action} to enable use
* with the Swing action model.
* <p>An action class is a <em>singleton</em>, i.e. should generally contain no instance state.
* Rather, subclassing and use of abstract protected methods should be used
* to create variants of the action.
* <p>While it is possible to subclass this class directly--for example, if your "action"
* is really a placeholder for a popup menu that shows other actions--most people will
* prefer to use one of the subclasses, which are more convenient.
*
* @author Ian Formanek, Jaroslav Tulach
*/
public abstract class SystemAction extends SharedClassObject
implements Action, HelpCtx.Provider {
/** Name of property indicating whether or not the action is enabled. */
public static final String PROP_ENABLED = "enabled"; // NOI18N
/** Name of property for the action's display icon. */
public static final String PROP_ICON = "icon"; // NOI18N
/** Name of property for the action's display icon, if textual. */
private static final String PROP_ICON_TEXTUAL = "iconTextual"; // NOI18N
private static ImageIcon BLANK_ICON = null;
private static ImageIcon getBlankIcon() {
if (BLANK_ICON == null) {
BLANK_ICON = new ImageIcon(Utilities.loadImage("org/openide/resources/actions/empty.gif", true)); // NOI18N
}
return BLANK_ICON;
}
private static final Set relativeIconResourceClasses = new HashSet(200); // Set<String>
// Matches NB 3.4 w/ openide-compat.jar; see #26491
private static final long serialVersionUID = -8361232596876856810L;
/** Obtain a singleton instance of the action with a specified class.
* If there already is a instance then it is returned, otherwise
* a new one is created.
*
* @param actionClass the class of the action to find
* @return the singleton action instance
* @exception ClassCastException if the class is not <code>SystemAction</code>
* @exception IllegalArgumentException if the instance cannot be created
*/
public static SystemAction get (Class actionClass) {
return (SystemAction)findObject (actionClass, true);
}
/** Get a human presentable name of the action.
* This may be
* presented as an item in a menu.
* <p>Using the normal menu presenters, an included ampersand
* before a letter will be treated as the name of a mnemonic.
* @return the name of the action
*/
public abstract String getName ();
/** Get a help context for the action.
* @return the help context for this action
*/
public abstract HelpCtx getHelpCtx ();
/** Test whether the action is currently enabled.
* @return <code>true</code> if so
*/
public boolean isEnabled() {
return getProperty (PROP_ENABLED).equals (Boolean.TRUE);
}
/** Set whether the action should be enabled.
* @param value <code>true</code> to enable it
*/
public void setEnabled(boolean value) {
putProperty (PROP_ENABLED, value ? Boolean.TRUE : Boolean.FALSE, true);
}
/** Set a property in the singleton. This property is common for all instances
* of the same class.
*
* @param name the name of the property
* @param value the value
*/
public final void putValue (String name, Object value) {
putProperty (name, value, true);
// Could handle putValue (SMALL_ICON, ImageIcon icon) but not
// really that important.
}
/** Get a property in the singleton. Values are shared among all instances of the same class.
* The special tokens {@link Action#NAME} and {@link Action#SMALL_ICON} are also recognized
* and delegated to {@link #getName} and {@link #getIcon}, resp.
* @param name the name of the property
* @return the value
*/
public final Object getValue (String name) {
Object val = getProperty (name);
if (val == null) {
if (NAME.equals (name))
val = getName ();
else if (SMALL_ICON.equals (name))
val = getIcon ();
}
return val;
}
/** Actually perform the action.
* Specified in {@link java.awt.event.ActionListener#actionPerformed}.
* <p>In some cases, the implementation may have an empty body,
* if the presenters handle the performing of the action in a different way
* than by calling this method.
* <p>When run in the normal way from the action manager (e.g. as
* part of a standard menu or toolbar presenter), the action body can
* block and take time, but needs to explicitly ask to enter the AWT
* event thread if doing any GUI work. See the Threading Models document
* in API documentation for details.
* @param ev the event triggering the action
*/
public abstract void actionPerformed (java.awt.event.ActionEvent ev);
/** Initialize the action.
* The default implementation just enabled it.
*/
protected void initialize () {
putProperty (PROP_ENABLED, Boolean.TRUE);
super.initialize ();
}
/** Indicate whether action state should be cleared after the last action of this class is deleted.
* @return <code>false</code> in the default implementation
*/
protected boolean clearSharedData () {
return false;
}
/** Set the action's display icon.
* @param icon the icon
*/
public final void setIcon (Icon icon) {
putProperty (PROP_ICON, icon, true);
putProperty (PROP_ICON_TEXTUAL, icon);
}
/** Get the action's display icon.
* @return the icon
* @throws IllegalStateException if an icon could not be created
*/
public final Icon getIcon () {
return getIcon (false);
}
/** Get the action's display icon, possibly creating a text label.
* @param createLabel if <code>true</code>, create a textual icon if otherwise there
* would be none; if <code>false</code>, create a blank icon
* @return an icon
* @throws IllegalStateException if an icon could not be created
*/
public final Icon getIcon (boolean createLabel) {
synchronized (getLock ()) {
Icon img = (Icon) getProperty (createLabel ? PROP_ICON_TEXTUAL : PROP_ICON);
if (img == null) {
// create the icon from the resource
String resName = iconResource ();
if (resName != null) {
if (resName.indexOf('/') == -1) {
// Old action that used a relative path to the icon.
// (If it used a relative path going down a directory, tough luck.
// It was never documented that you could use relative paths.
// apisupport templates did it, but they put icons in the same dir.)
String clazz = getClass().getName();
URL u = getClass().getResource(resName);
if (u != null) {
img = new ImageIcon(u);
if (relativeIconResourceClasses.add(clazz)) {
ErrorManager.getDefault().log(ErrorManager.WARNING, "Deprecated relative path in " + clazz + ".iconResource (cf. #20072)"); // NOI18N
}
} else {
throw new IllegalStateException("No such icon from " + clazz + ": " + resName); // NOI18N
}
} else {
// Hopefully an absolute path, but again (#26887) might be relative.
Image i = Utilities.loadImage(resName, true);
if (i != null) {
// OK, the normal case.
img = new ImageIcon(i);
} else {
// Check for an old-style relative path.
URL u = getClass().getResource(resName);
String clazz = getClass().getName();
if (u != null) {
// OK, but warn.
img = new ImageIcon(u);
if (relativeIconResourceClasses.add(clazz)) {
ErrorManager.getDefault().log(ErrorManager.WARNING, "Deprecated relative path in " + clazz + ".iconResource (cf. #26887)"); // NOI18N
}
} else {
// Really can't find it.
throw new IllegalStateException("No such icon from " + clazz + ": " + resName); // NOI18N
}
}
}
putProperty (PROP_ICON, img);
putProperty (PROP_ICON_TEXTUAL, img);
} else {
// No icon specified.
if (createLabel) {
String text = getName ();
if (text.endsWith ("...")) text = text.substring (0, text.length () - 3); // NOI18N
img = new ComponentIcon(new JLabel(Actions.cutAmpersand(text.trim())));
putProperty (PROP_ICON_TEXTUAL, img);
} else {
img = getBlankIcon();
putProperty (PROP_ICON, img);
}
}
}
return img;
}
}
//
// Old deprecated methods - compatibility stuff - will be made public and renamed
// by NbEnhanceClass task
//
private void s3tIcon (ImageIcon icon) {
setIcon ((Icon) icon);
}
private ImageIcon g3tIcon () {
Icon i = getIcon (false);
if (i instanceof ImageIcon) {
return ((ImageIcon) i);
} else {
// [PENDING] could try to translate Icon -> ImageIcon somehow,
// but I have no idea how to do this (paint it, take Component
// graphics, load the image data somehow??)
return new ImageIcon(Utilities.loadImage("org/openide/resources/actions/empty.gif", true)); // NOI18N
}
}
//
// End of compatibility stuff
//
/** Specify the proper resource name for the action's icon.
* May be overridden by subclasses; the default is to have no icon.
* Typically this should be a 16x16 color GIF.
* Do not use relative paths nor an initial slash.
* As of APIs version 3.24, this path will be used for a localized search automatically.
* @return the resource name for the icon, e.g. <code>com/mycom/mymodule/myIcon.gif</code>; or <code>null</code> to have no icon (make a text label)
*/
protected String iconResource () {
return null;
}
//
// Static methods
//
/** Create the default toolbar representation of an array of actions.
* Null items in the array will add a separator to the toolbar.
*
* @param actions actions to show in the generated toolbar
* @return a toolbar instance displaying them
*/
public static JToolBar createToolbarPresenter (SystemAction[] actions) {
JToolBar p = new JToolBar ();
int i, k = actions.length;
for (i = 0; i < k; i++) {
if (actions [i] == null)
p.addSeparator();
else
if (actions [i] instanceof Presenter.Toolbar)
p.add (((Presenter.Toolbar)actions [i]).getToolbarPresenter ());
}
return p;
}
/** Concatenate two arrays of actions.
* @param actions1 first array of actions to link
* @param actions2 second array of actions to link
* @return an array of both sets of actions in the same order
*/
public static SystemAction[] linkActions (SystemAction[] actions1, SystemAction[] actions2) {
List l = new Vector (Arrays.asList (actions1));
l.addAll (Arrays.asList (actions2));
return (SystemAction[]) l.toArray (actions1);
}
/** Create the default popup menu representation of an array of actions.
* @param actions actions to show in the generated menu
* @return a popup menu displaying them
*/
public static JPopupMenu createPopupMenu(SystemAction []actions) {
boolean addSeparator = false;
JPopupMenu popupMenu = new JPopupMenuPlus();
JMenuItem item;
for(int i = 0; i < actions.length; i++) {
if (actions[i] == null) {
addSeparator = popupMenu.getComponentCount() > 0;
continue;
}
if (actions[i] instanceof Presenter.Popup) {
item = ((Presenter.Popup)actions[i]).getPopupPresenter ();
} else {
item = new JMenuItem (actions[i].getName ());
item.setEnabled(false);
}
if(addSeparator) {
popupMenu.addSeparator ();
addSeparator = false;
}
popupMenu.add (item);
}
return popupMenu;
}
/** Icon based on a component (such as a text label).
* Just draws that component as an image.
*/
private static class ComponentIcon extends ImageIcon {
private JComponent comp;
private BufferedImage image;
/** Create an icon.
* @param comp a component, which must be unattached to a container
* and should not be used for other purposes
*/
public ComponentIcon (JComponent comp) {
if (comp.getParent () != null) throw new IllegalArgumentException ();
this.comp = comp;
Dimension size = comp.getPreferredSize ();
// Careful! If you have e.g. a JLabel with empty text, width = 0 => exceptions.
// Must make sure it is at least a reasonable size.
comp.setSize (Math.max (size.width, 16), Math.max (size.height, 16));
}
protected void loadImage (Image i) {
}
public void paintIcon (Component c, Graphics g, int x, int y) {
// When enabled, tracks color choices of container:
comp.setBackground (c.getBackground ());
comp.setForeground (c.getForeground ());
Graphics clip = g.create (x, y, getIconWidth (), getIconHeight ());
comp.paint (clip);
}
public int getIconWidth () {
return comp.getWidth ();
}
public int getIconHeight () {
return comp.getHeight ();
}
// Needed because GrayFilter (used for disabled icons) calls this directly,
// rather than going through the Icon interface.
public Image getImage () {
if (image == null) {
image = new BufferedImage (getIconWidth (), getIconHeight (), BufferedImage.TYPE_INT_ARGB);
// [PENDING] this is obviously ugly, but how should we decide what is the
// default fg for the Main Window toolbar area? Background is irrelevant,
// since we use alpha channel. But have to guess at the foreground.
comp.setForeground (Color.black);
comp.paint (image.getGraphics ());
}
return image;
}
}
}