/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.ui.action;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.Hashtable;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.KeyStroke;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mucommander.commons.file.util.ResourceLoader;
import com.mucommander.ui.icon.IconManager;
import com.mucommander.ui.main.MainFrame;
/**
* MuAction extends <code>AbstractAction</code> to add more functionalities and make it easier to integrate within
* muCommander. The biggest difference with <code>AbstractAction</code> is that MuAction instances are bound to a
* specific {@link MainFrame}.<br>
* Note that by being an Action, MuAction can be used in every Swing components that accept Action instances.
*
* <p>The MuAction class is abstract. MuAction subclasses must implement the {@link #performAction()} method
* to provide a response to the action trigger, and must provide a constructor with the
* {@link #MuAction(MainFrame, Map)} signature.
*
* <p>MuAction subclasses should not be instantiated directly, {@link ActionManager}'s <code>getActionInstance</code>
* methods should be used instead. Using {@link ActionManager} to retrieve a MuAction ensures that only one instance
* exists for a given {@link com.mucommander.ui.main.MainFrame}. This is particularly important because actions are stateful and can be used
* in several components of a MainFrame at the same time; if an action's state changes, the change must be reflected
* everywhere the action is used. It is also important for performance reasons: sharing one action throughout a
* {@link MainFrame} saves some memory and also CPU cycles as some actions listen to particular events to change
* their state accordingly.
*
* @see ActionManager
* @see ActionKeymap
* @author Maxence Bernard
*/
public abstract class MuAction extends AbstractAction {
protected final Logger LOGGER = LoggerFactory.getLogger(getClass());
/** The MainFrame associated with this MuAction */
protected MainFrame mainFrame;
/** if true, action events are ignored while the MainFrame is in 'no events mode'. Enabled by default. */
private boolean honourNoEventsMode = true;
/** if true, #performAction() is called from a separate thread (and not from the event thread) when this action is
* performed. Disabled by default. */
private boolean performActionInSeparateThread = false;
/** Name of the alternate accelerator KeyStroke property */
public final static String ALTERNATE_ACCELERATOR_PROPERTY_KEY = "alternate_accelerator";
/**
* Creates a new <code>MuAction</code> associated with the specified {@link MainFrame}. The properties contained by
* the given {@link Hashtable} are used to initialize this action's property map.
*
* @param mainFrame the MainFrame to associate with this new MuAction
* @param properties the initial properties to use in this action. The Hashtable may simply be empty if no initial
* properties are specified.
*/
public MuAction(MainFrame mainFrame, Map<String,Object> properties) {
this.mainFrame = mainFrame;
// Add properties to this Action.
for(String key : properties.keySet())
putValue(key, properties.get(key));
}
/**
* Return the {@link MainFrame} this MuAction is associated.
*
* @return the MainFrame this action is associated with
*/
public MainFrame getMainFrame() {
return this.mainFrame;
}
/**
* Returns the label of this action, <code>null</code> if this action has no label.
* The label value is stored in the {@link #NAME} property.
*
* @return the label of this action, <code>null</code> if this action has no label
*/
public String getLabel() {
return (String)getValue(Action.NAME);
}
/**
* Sets the label for this action, <code>null</code> for no label.
* The label value is stored in the {@link #NAME} property.
*
* @param label the new text label for this action, replacing the previous one (if any)
*/
public void setLabel(String label) {
putValue(Action.NAME, label);
}
/**
* Returns the tooltip text of this action, <code>null</code> if this action has no tooltip.
* The tooltip value is stored in the {@link #SHORT_DESCRIPTION} property.
*
* @return the tooltip text of this action, <code>null</code> if this action has no tooltip
*/
public String getToolTipText() {
return (String)getValue(Action.SHORT_DESCRIPTION);
}
/**
* Sets the tooltip for this action, <code>null</code> for no tooltip.
* The tooltip value is stored in the {@link #SHORT_DESCRIPTION} property.
*
* @param toolTipText the new tooltip text for this action replacing the previous one (if any)
*/
public void setToolTipText(String toolTipText) {
putValue(Action.SHORT_DESCRIPTION, toolTipText);
}
/**
* Return the icon of this action, <code>null</code> if this action has no icon.
* The icon value is stored in the {@link #SMALL_ICON} property.
*
* @return the icon of this action, <code>null</code> if this action has no icon
*/
public ImageIcon getIcon() {
return (ImageIcon)getValue(Action.SMALL_ICON);
}
/**
* Sets the icon for this action, <code>null</code> if this action has no icon.
* The icon value is stored in the {@link #SMALL_ICON} property.
*
* @param icon the new image icon for this action, replacing the previous one (if any)
*/
public void setIcon(ImageIcon icon) {
putValue(Action.SMALL_ICON, icon);
}
/**
* Returns the accelerator KeyStroke of this action, <code>null</code> if this action has no accelerator.
* The accelerator value is stored in the <code>Action.ACCELERATOR_KEY</code> property.
*
* @return the accelerator KeyStroke of this action, <code>null</code> if this action has no accelerator
*/
public KeyStroke getAccelerator() {
return (KeyStroke)getValue(Action.ACCELERATOR_KEY);
}
/**
* Sets the accelerator KeyStroke for this action, <code>null</code> for no accelerator.
* The tooltip value is stored in the <code>Action.ACCELERATOR_KEY</code> property.
*
* @param keyStroke the new accelerator KeyStroke for this action, replacing the previous one (if any)
*/
public void setAccelerator(KeyStroke keyStroke) {
putValue(Action.ACCELERATOR_KEY, keyStroke);
}
/**
* Returns the alternate accelerator KeyStroke of this action, <code>null</code> if it doesn't have any.
* The accelerator accelerator value is stored in the {@link #ALTERNATE_ACCELERATOR_PROPERTY_KEY} property.
*
* @return the alternate accelerator KeyStroke of this action, <code>null</code> if it doesn't have any
*/
public KeyStroke getAlternateAccelerator() {
return (KeyStroke)getValue(ALTERNATE_ACCELERATOR_PROPERTY_KEY);
}
/**
* Sets the alternate accelerator KeyStroke for this action, <code>null</code> for none.
* The accelerator accelerator value is stored in the {@link #ALTERNATE_ACCELERATOR_PROPERTY_KEY} property.
*
* @param keyStroke the new alternate accelerator KeyStroke for this action, replacing the previous one (if any)
*/
public void setAlternateAccelerator(KeyStroke keyStroke) {
putValue(ALTERNATE_ACCELERATOR_PROPERTY_KEY, keyStroke);
}
/**
* Returns <code>true</code> if both keystrokes' {@link KeyStroke#getKeyChar() char},
* {@link KeyStroke#getKeyCode() code} and {@link KeyStroke#getModifiers() modifiers} are equal.
* Unlike {@link KeyStroke#equals(Object)}, this method does not take into account the
* {@link KeyStroke#isOnKeyRelease() onKeyRelease} flag.
*
* @param ks1 first keystroke to test
* @param ks2 second keystroke to test
* @return <code>true</code> if both keystrokes' char, code and modifiers are equal
*/
protected boolean acceleratorsEqual(KeyStroke ks1, KeyStroke ks2) {
return ks1.getKeyChar()==ks2.getKeyChar()
&& ks1.getKeyCode()==ks2.getKeyCode()
&& ks1.getModifiers()==ks2.getModifiers();
}
/**
* Returns <code>true</code> if the given KeyStroke is one of this action's accelerators. Keystrokes are compared
* using {@link #acceleratorsEqual(KeyStroke, KeyStroke)}, so that the {@link KeyStroke#isOnKeyRelease()} flag
* is not taken into account. This method always returns <code>false</code> if this method has no accelerator.
*
* @param keyStroke the KeyStroke to test against this action's accelerators
* @return true if the given KeyStroke is one of this action's accelerators
*/
public boolean isAccelerator(KeyStroke keyStroke) {
KeyStroke accelerator = getAccelerator();
if(accelerator!=null && acceleratorsEqual(accelerator, keyStroke))
return true;
accelerator = getAlternateAccelerator();
return accelerator!=null && acceleratorsEqual(accelerator, keyStroke);
}
/**
* Returns a displayable String representation of this action's accelerator, in the
* <code>[modifier]+[modifier]+...+key</code> format.
* This method returns <code>null</code> if this action has no accelerator.
*
* @return a String representation of the accelerator, or <code>null</code> if this action has no accelerator.
*/
public String getAcceleratorText() {
KeyStroke accelerator = getAccelerator();
if(accelerator==null)
return null;
String text = KeyEvent.getKeyText(accelerator.getKeyCode());
int modifiers = accelerator.getModifiers();
if(modifiers!=0)
text = KeyEvent.getKeyModifiersText(modifiers)+"+"+text;
return text;
}
/**
* Return <code>true</code> if action events are ignored while the <code>MainFrame</code> associated with this
* action is in 'no events mode' (see {@link MainFrame} for an explanation about this mode).
* By default, this method returns <code>true</code>.
*
* @return <code>true</code> if action events are ignored while the <code>MainFrame</code> associated with this
* action is in 'no events' mode
*/
public boolean honourNoEventsMode() {
return honourNoEventsMode;
}
/**
* Sets whether action events are to be ignored while the <code>MainFrame</code> associated with this action is in
* 'no events mode' (see {@link MainFrame} for an explanation about this mode).
* By default (unless this method has been called), 'no events mode' is honoured.
*
* @param honourNoEventsMode if true, actions events will be ignored while the <code>MainFrame</code> associated
* with this action is in 'no events mode'
*/
public void setHonourNoEventsMode(boolean honourNoEventsMode) {
this.honourNoEventsMode = honourNoEventsMode;
}
/**
* Returns <code>true</code> if {@link #performAction()} is called from a separate thread (and not from the event
* thread) when this action is performed. By default, <code>false</code> is returned, i.e. actions are performed
* from the main event thread.
*
* <p>Actions that have the potential to hold the caller thread for a substantial amount of time should perform the
* action in a separate thread, to avoid locking the event thread.</p>
*
* @return <code>true</code> if {@link #performAction()} is called from a separate thread (and not from the event
* thread) when this action is performed
*/
public boolean performActionInSeparateThread() {
return performActionInSeparateThread;
}
/**
* Sets whether {@link #performAction()} is called from a separate thread (and not from the event thread) when this
* action is performed. By default (unless this method has been called), actions are performed from the main event
* thread.
*
* <p>Actions that have the potential to hold the caller thread for a substantial amount of time should perform the
* action in a separate thread, to avoid locking the event thread.</p>
*
* @param performActionInSeparateThread <code>true</code> to have {@link #performAction()} called from a separate
* thread (and not from the event thread) when this action is performed
*/
public void setPerformActionInSeparateThread(boolean performActionInSeparateThread) {
this.performActionInSeparateThread = performActionInSeparateThread;
}
/**
* Shorthand for {@link #getStandardIcon(Class)} called with the Class instance returned by {@link #getClass()}.
*
* @return the standard icon corresponding to this MuAction class, <code>null</code> if none was found
*/
public ImageIcon getStandardIcon() {
return getStandardIcon(getClass());
}
/**
* Shorthand for {@link #getStandardIconPath(Class)} called with the Class instance returned by {@link #getClass()}.
*
* @return the standard path for this action's image icon
*/
public String getStandardIconPath() {
return getStandardIconPath(getClass());
}
////////////////////
// Static methods //
////////////////////
/**
* Queries {@link IconManager} for an image icon corresponding to the specified action using standard icon path
* conventions. Returns the image icon, <code>null</code> if none was found.
*
* @param action a MuAction class descriptor
* @return the standard icon image corresponding to the specified MuAction class, <code>null</code> if none was found
*/
public static ImageIcon getStandardIcon(Class<? extends MuAction> action) {
// Look for an icon image file with the /action/<classname>.png path and use it if it exists
String iconPath = getStandardIconPath(action);
if(ResourceLoader.getResourceAsURL(iconPath) == null)
return null;
return IconManager.getIcon(iconPath);
}
/**
* Returns the standard path to the icon image for the specified {@link MuAction} class. The returned path is
* relative to the application's JAR file.
*
* @param action a MuAction class descriptor
* @return the standard path to the icon image corresponding to the specified MuAction class
*/
public static String getStandardIconPath(Class<? extends MuAction> action) {
return IconManager.getIconSetFolder(IconManager.ACTION_ICON_SET) + getActionName(action) + ".png";
}
private static String getActionName(Class<? extends MuAction> action) {
return action.getSimpleName().replace("Action", "");
}
///////////////////////////////////
// AbstractAction implementation //
///////////////////////////////////
/**
* Intercepts action events and filters them out when the {@link MainFrame} associated with this action is in
* 'no events' mode and {@link #honourNoEventsMode()} returns <code>true</code>.
* If the action event is not filtered out, {@link #performAction()} is called to provide a response to the action event.
*/
public void actionPerformed(ActionEvent e) {
// Discard this event while in 'no events mode'
if(!(mainFrame.getNoEventsMode() && honourNoEventsMode())) {
if(performActionInSeparateThread()) {
new Thread() {
@Override
public void run() {
performAction();
}
}.start();
}
else {
performAction();
}
}
}
//////////////////////
// Abstract methods //
//////////////////////
/**
* Called when this action has been triggered. This method provides a response to the action trigger.
*/
public abstract void performAction();
/**
* Returns the <code>ActionDescriptor</code> of the action.
* @return the <code>ActionDescriptor</code> of the action.
*/
public abstract ActionDescriptor getDescriptor();
}