/* * Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved. * * This file is part of the Jspresso framework. * * Jspresso is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Jspresso 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Jspresso. If not, see <http://www.gnu.org/licenses/>. */ package org.jspresso.framework.application.frontend.action; import java.util.Collection; import java.util.LinkedHashSet; import java.util.Locale; import java.util.Map; import org.apache.commons.lang3.builder.ToStringBuilder; import org.jspresso.framework.action.ActionContextConstants; import org.jspresso.framework.application.action.AbstractAction; import org.jspresso.framework.application.frontend.IFrontendController; import org.jspresso.framework.binding.IMvcBinder; import org.jspresso.framework.util.descriptor.DefaultIconDescriptor; import org.jspresso.framework.util.gate.IGate; import org.jspresso.framework.util.gate.ModelTrackingGate; import org.jspresso.framework.util.gate.NotEmptyCollectionSelectionTrackingGate; import org.jspresso.framework.util.gate.SingleCollectionSelectionTrackingGate; import org.jspresso.framework.util.gui.Icon; import org.jspresso.framework.util.i18n.ITranslationProvider; import org.jspresso.framework.view.IActionFactory; import org.jspresso.framework.view.IIconFactory; import org.jspresso.framework.view.IViewFactory; import org.jspresso.framework.view.action.IDisplayableAction; /** * This is the base class for frontend actions. To get a better understanding of * how actions are organized in Jspresso, please refer to * {@code AbstractAction} documentation. * <p> * This base class allows for visual (name, icon, toolTip) as well as * accessibility (accelerator, mnemonic shortcuts) and actionability (using * gates) parametrization. * <p> * A frontend action is to be executed by the frontend controller in the context * of the UI. It can thus access the view structure, interact visually with the * user, and so on. A frontend action can chain a backend action but the * opposite will be prevented. * * @param <E> * the actual gui component type used. * @param <F> * the actual icon type used. * @param <G> * the actual action type used. * @author Vincent Vandenschrick */ public class FrontendAction<E, F, G> extends AbstractAction implements IDisplayableAction { /** * {@code COMPONENT_TO_FOCUS} is COMPONENT_TO_FOCUS. */ public static final String COMPONENT_TO_FOCUS = "COMPONENT_TO_FOCUS"; private String acceleratorAsString; private Collection<IGate> actionabilityGates; private final DefaultIconDescriptor actionDescriptor; private boolean collectionBased; private boolean multiSelectionEnabled; private Boolean hiddenWhenDisabled; private String mnemonicAsString; private String styleName; private Integer repeatPeriodMillis; /** * Constructs a new {@code AbstractFrontendAction} instance. */ public FrontendAction() { actionDescriptor = new DefaultIconDescriptor(); setCollectionBased(false); setMultiSelectionEnabled(true); } /** * {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!getClass().isAssignableFrom(obj.getClass()) && !obj.getClass().isAssignableFrom(getClass())) { return false; } final IDisplayableAction other = (IDisplayableAction) obj; if (getName() == null) { return false; } return getName().equals(other.getName()); } /** * {@inheritDoc} */ @Override public String getAcceleratorAsString() { return acceleratorAsString; } /** * Gets the actionabilityGates. * * @return the actionabilityGates. */ @Override public Collection<IGate> getActionabilityGates() { return actionabilityGates; } /** * {@inheritDoc} */ @Override public String getDescription() { return actionDescriptor.getDescription(); } /** * {@inheritDoc} */ @Override public String getI18nDescription(ITranslationProvider translationProvider, Locale locale) { if (getDescription() != null) { return translationProvider.getTranslation(getDescription(), "", locale); } return getI18nName(translationProvider, locale); } /** * {@inheritDoc} */ @Override public String getI18nName(ITranslationProvider translationProvider, Locale locale) { return translationProvider.getTranslation(getName(), locale); } /** * {@inheritDoc} */ @Override public Icon getIcon() { return actionDescriptor.getIcon(); } /** * Gets the icon image URL or null if no icon is set. * * @return the icon image URL or null if no icon is set. */ protected String getIconImageURL() { Icon icon = getIcon(); if (icon != null) { return icon.getIconImageURL(); } return null; } /** * {@inheritDoc} */ @Override public String getMnemonicAsString() { return mnemonicAsString; } /** * {@inheritDoc} */ @Override public String getName() { return actionDescriptor.getName(); } /** * Gets the permId. * * @return the permId. */ @Override public String getPermId() { String permId = super.getPermId(); if (permId == null) { return getName(); } return permId; } /** * {@inheritDoc} */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result; if (getName() != null) { result += getName().hashCode(); } return result; } /** * Returns false. * <p> * {@inheritDoc} */ @Override public boolean isBackend() { return false; } /** * Gets the collectionBased. * * @return the collectionBased. */ @Override public boolean isCollectionBased() { return collectionBased; } /** * Gets the multiSelectionEnabled. * * @return the multiSelectionEnabled. */ @Override public boolean isMultiSelectionEnabled() { return multiSelectionEnabled; } /** * Configures a keyboard accelerator shortcut on this action. Support of this * feature depends on the UI execution platform. The syntax used consists of * listing keys that should be pressed to trigger the action, i.e. * {@code alt d} or {@code ctrl c}. This is the syntax supported by * the {@code javax.swing.KeyStroke#getKeyStroke(...)} swing static * method. * * @param acceleratorAsString * the acceleratorAsString to set. */ public void setAcceleratorAsString(String acceleratorAsString) { this.acceleratorAsString = acceleratorAsString; } /** * Assigns a collection of gates to determine action <i>actionability</i>. An * action will be considered actionable (enabled) if and only if all gates are * open. This mechanism is mainly used for dynamic UI authorization based on * model state, e.g. a validated invoice should not be validated twice. * <p> * Action assigned gates will be cloned for each concrete action instance * created and bound to its respective UI component (usually a button). So * basically, each action instance will have its own, unshared collection of * actionability gates. * <p> * Jspresso provides a useful set of gate types, like the binary property gate * that open/close based on the value of a boolean property of the view model * the action is installed to. * <p> * By default, frontend actions are assigned a generic gate that closes * (disables the action) when the view is not assigned any model. * * @param actionabilityGates * the actionabilityGates to set. */ public void setActionabilityGates(Collection<IGate> actionabilityGates) { this.actionabilityGates = actionabilityGates; completeActionabilityGates(); } /** * Declares the action as working on a collection of objects. Collection based * actions will typically be installed on selectable views (table, list, tree) * and will be enabled only when the view selection is not empty (a default * gate is installed for this purpose). Moreover, model gates that are * configured on collection based actions take their model from the view * selected components instead of the view model itself. In case of * multi-selection enabled UI views, the actionability gates will actually * open if and only if their opening condition is met for all the selected * items. * * @param collectionBased * the collectionBased to set. */ public void setCollectionBased(boolean collectionBased) { this.collectionBased = collectionBased; completeActionabilityGates(); } /** * Declares the action as being able to run on a collection containing more * than 1 element. A multiSelectionEnabled = false action will be disabled * when the selection contains no or more than one element. * * @param multiSelectionEnabled * the multiSelectionEnabled to set. */ public void setMultiSelectionEnabled(boolean multiSelectionEnabled) { this.multiSelectionEnabled = multiSelectionEnabled; completeActionabilityGates(); } /** * Sets the key used to compute the internationalized description of the * action. The translated description is then usually used as toolTip for the * action. * * @param description * the description to set. */ public void setDescription(String description) { actionDescriptor.setDescription(description); } /** * Sets the icon image URL used to decorate the action UI component peer. * <p> * Supported URL protocols include : * <ul> * <li>all JVM supported protocols</li> * <li>the <b>jar:/</b> pseudo URL protocol</li> * <li>the <b>classpath:/</b> pseudo URL protocol</li> * </ul> * * @param iconImageURL * the iconImageURL to set. */ public void setIconImageURL(String iconImageURL) { actionDescriptor.setIconImageURL(iconImageURL); } /** * Sets the icon. * * @param icon * the icon to set. */ public void setIcon(Icon icon) { actionDescriptor.setIcon(icon); } /** * Configures the mnemonic key used for this action. Support of this feature * depends on the UI execution platform. Mnemonics are typically used in menu * and menu items. * * @param mnemonicStringRep * the mnemonic to set represented as a string as KeyStroke factory * would parse it. */ public void setMnemonicAsString(String mnemonicStringRep) { this.mnemonicAsString = mnemonicStringRep; } /** * Sets the key used to compute the internationalized name of the action. The * translated name is then usually used as label for the action (button label, * menu label, ...). * * @param name * the name to set. */ public void setName(String name) { actionDescriptor.setName(name); } /** * {@inheritDoc} */ @Override public String toString() { return new ToStringBuilder(this).append("name", getName()).append("description", getDescription()).append( "iconImageURL", getIcon()).toString(); } private void completeActionabilityGates() { if (actionabilityGates == null) { actionabilityGates = new LinkedHashSet<>(); } if (isCollectionBased()) { actionabilityGates.remove(ModelTrackingGate.INSTANCE); actionabilityGates.add(NotEmptyCollectionSelectionTrackingGate.INSTANCE); } else { actionabilityGates.remove(NotEmptyCollectionSelectionTrackingGate.INSTANCE); actionabilityGates.add(ModelTrackingGate.INSTANCE); } if (isMultiSelectionEnabled()) { actionabilityGates.remove(SingleCollectionSelectionTrackingGate.INSTANCE); } else { actionabilityGates.add(SingleCollectionSelectionTrackingGate.INSTANCE); } } /** * Gets the actionFactory. * * @param context * the action context. * @return the actionFactory. */ protected IActionFactory<G, E> getActionFactory(Map<String, Object> context) { return getViewFactory(context).getActionFactory(); } /** * Retrieves the widget which triggered the action from the action context. * * @param context * the action context. * @return the widget which triggered the action. */ @SuppressWarnings("unchecked") protected E getActionWidget(Map<String, Object> context) { return (E) context.get(ActionContextConstants.ACTION_WIDGET); } /** * Retrieves the concrete action (swing, remote) that was triggered triggered * from the action context. * * @param context * the action context. * @return the concrete action that was triggered. */ @SuppressWarnings("unchecked") protected G getUiAction(Map<String, Object> context) { return (G) context.get(ActionContextConstants.UI_ACTION); } /** * Retrieves the UI action event from the action context. * * @param context * the action context. * @return the UI action event that was triggered. */ protected Object getUiEvent(Map<String, Object> context) { return context.get(ActionContextConstants.UI_EVENT); } /** * Gets the frontend controller out of the action context. * * @param context * the action context. * @return the frontend controller. */ @Override protected IFrontendController<E, F, G> getController(Map<String, Object> context) { return getFrontendController(context); } /** * Gets the iconFactory. * * @param context * the action context. * @return the iconFactory. */ protected IIconFactory<F> getIconFactory(Map<String, Object> context) { return getViewFactory(context).getIconFactory(); } /** * Gets the mvcBinder. * * @param context * the action context. * @return the mvcBinder. */ protected IMvcBinder getMvcBinder(Map<String, Object> context) { return getController(context).getMvcBinder(); } /** * Retrieves the widget this action was triggered from. It may serve to * determine the root window or dialog for instance. It uses a well-known * action context key which is : <li> * {@code ActionContextConstants.SOURCE_COMPONENT}. * * @param context * the action context. * @return the source widget this action was triggered from. */ @SuppressWarnings("unchecked") protected E getSourceComponent(Map<String, Object> context) { return (E) context.get(ActionContextConstants.SOURCE_COMPONENT); } /** * Gets the viewFactory. * * @param context * the action context. * @return the viewFactory. */ protected IViewFactory<E, F, G> getViewFactory(Map<String, Object> context) { return getController(context).getViewFactory(); } /** * Gets the lastUpdated. * * @return the lastUpdated. */ @Override public long getLastUpdated() { return actionDescriptor.getLastUpdated(); } /** * Sets the lastUpdated. * * @param lastUpdated * the lastUpdated to set. * @internal */ public void setLastUpdated(long lastUpdated) { actionDescriptor.setLastUpdated(lastUpdated); } /** * Gets the styleName. * * @return the styleName. */ @Override public String getStyleName() { return styleName; } /** * Assigns the style name to use for this view. The way it is actually * leveraged depends on the UI channel. It will generally be mapped to some * sort of CSS style name. * <p> * Default value is {@code null}, meaning that a default style is used. * * @param styleName * the styleName to set. */ public void setStyleName(String styleName) { this.styleName = styleName; } /** * Sets the icon preferred width. * * @param iconPreferredWidth * the iconPreferredWidth to set. * @see org.jspresso.framework.util.descriptor.DefaultIconDescriptor#setIconPreferredWidth(int) */ public void setIconPreferredWidth(int iconPreferredWidth) { actionDescriptor.setIconPreferredWidth(iconPreferredWidth); } /** * Sets the icon preferred height. * * @param iconPreferredHeight * the iconPreferredHeight to set. * @see org.jspresso.framework.util.descriptor.DefaultIconDescriptor#setIconPreferredHeight(int) */ public void setIconPreferredHeight(int iconPreferredHeight) { actionDescriptor.setIconPreferredHeight(iconPreferredHeight); } /** * Gets repeat period millis. * * @return the repeat period millis */ @Override public Integer getRepeatPeriodMillis() { return repeatPeriodMillis; } /** * Sets repeat period in milliseconds. Whenever this is set to a positive integer, the client UI controller should * schedule an execution of this action periodically. * * @param repeatPeriodMillis * the repeat period millis */ public void setRepeatPeriodMillis(Integer repeatPeriodMillis) { this.repeatPeriodMillis = repeatPeriodMillis; } /** * When configured to {@code true}, the action is hidden when it is disabled. Default value is * undefined, i.e. {@code null}, meaning that the enclosing action list or map drives the configuration. * * @return the boolean */ public Boolean getHiddenWhenDisabled() { return hiddenWhenDisabled; } /** * Sets hidden when disabled. * * @param hiddenWhenDisabled * the hidden when disabled */ public void setHiddenWhenDisabled(Boolean hiddenWhenDisabled) { this.hiddenWhenDisabled = hiddenWhenDisabled; } }