/** * eAdventure (formerly <e-Adventure> and <e-Game>) is a research project of the * <e-UCM> research group. * * Copyright 2005-2010 <e-UCM> research group. * * You can access a list of all the contributors to eAdventure at: * http://e-adventure.e-ucm.es/contributors * * <e-UCM> is a research group of the Department of Software Engineering * and Artificial Intelligence at the Complutense University of Madrid * (School of Computer Science). * * C Profesor Jose Garcia Santesmases sn, * 28040 Madrid (Madrid), Spain. * * For more info please visit: <http://e-adventure.e-ucm.es> or * <http://www.e-ucm.es> * * **************************************************************************** * * This file is part of eAdventure, version 2.0 * * eAdventure 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. * * eAdventure 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 eAdventure. If not, see <http://www.gnu.org/licenses/>. */ package es.eucm.ead.editor.view.generic; import javax.swing.JComponent; import es.eucm.ead.editor.control.Command; import es.eucm.ead.editor.model.ModelEvent; import es.eucm.ead.editor.model.ModelEventUtils; import es.eucm.ead.editor.model.nodes.DependencyNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import es.eucm.ead.editor.control.CommandManager; import es.eucm.ead.editor.control.commands.ChangeFieldCommand; import es.eucm.ead.editor.control.commands.EmptyCommand; import es.eucm.ead.editor.util.Log4jConfig; import es.eucm.ead.editor.view.generic.accessors.Accessor; import java.awt.Color; import java.util.ArrayList; import javax.swing.BorderFactory; import javax.swing.border.Border; /** * Abstract implementation for {@link Option}s * * @param <S> type of the option element */ public abstract class AbstractOption<S> implements Option<S> { static private Logger logger = LoggerFactory .getLogger(AbstractOption.class); private static enum UpdateType { Event, Control, Synthetic }; /** * Label on the component */ private String label; /** * Tool tip text explanation */ private String toolTipText; /** * Last valid status */ protected boolean currentlyValid = true; /** * Validity-checking class. Useful for establishing complex constraints */ protected CompositeConstraint validityConstraint = new CompositeConstraint(); /** * While updating, external updates will be ignored */ protected boolean isUpdating = false; /** * A copy of the old value. Used when creating change events / commands, * and generally updated by the AbstractAction itself. */ protected S oldValue; /** * Accessor used to read and write model values */ protected Accessor<S> accessor; /** * Keeps a reference to the current commandManager */ protected CommandManager manager; /** * A reference to the node to include in 'changed' ModelEvents */ protected DependencyNode[] changed; /** * The default color for invalidness */ protected static final Color invalidBorderColor = Color.red; /** * The default (non-error) border for this control */ protected Border defaultBorder; /** * The returned component */ protected JComponent component; /** * Creates an AbstractAction. * @param label The label in the option (can be null) * @param toolTipText The toolTipText in the option (cannot be null) * @param changed dependency nodes to be considered "changed" when this changes */ public AbstractOption(String label, String toolTipText, Accessor<S> accessor, DependencyNode... changed) { this.label = label; this.toolTipText = toolTipText; this.accessor = accessor; if (toolTipText == null || toolTipText.isEmpty()) { throw new RuntimeException( "ToolTipTexts MUST be provided for all interface elements!"); } this.changed = changed == null ? new DependencyNode[0] : changed; } public ArrayList<Constraint> getConstraints() { return validityConstraint.getList(); } /** * Will be called when the model changes. Uses changeConsideredRelevant * to avoid acting on non-changes. * @param event */ @Override public void modelChanged(ModelEvent event) { if (isUpdating) { logger.debug("option {} isUpdating -- ignores change", hashCode()); return; } logger.debug("option {} notified of change: {}", new Object[] { hashCode(), event }); if (ModelEventUtils.changes(event, changed)) { uncontestedUpdate(accessor.read(), UpdateType.Event); } else { logger.debug("why am I even receiving this?"); } } /** * Retargets exposed object. Essentially resets * @param accessor access to newly-exposed object * @param changed updated dependency information; overwrites previous information */ public JComponent retarget(Accessor<S> accessor, CommandManager manager, DependencyNode... changed) { this.accessor = accessor; this.oldValue = accessor.read(); if (component == null) { getComponent(manager); } setControlValue(oldValue); this.changed = changed == null ? new DependencyNode[0] : changed; return component; } /** * Retrieves title (used for label). * * @see es.eucm.eadventure.editor.view.generics.Option#getTitle() */ @Override public String getTitle() { return label; } /** * Retrieves tooltip-text (used for tooltips) * * @see es.eucm.eadventure.editor.view.generics.Option#getToolTipText() */ @Override public String getToolTipText() { return toolTipText; } /** * Creates the control, setting the initial value. * Subclasses should register as listeners to any changes in the control, * and call update() when such changes occur. */ protected abstract JComponent createControl(); /** * Utility method to draw a border around the component */ protected void decorateComponent() { if (currentlyValid) { component.setBorder(defaultBorder); } else { component.setBorder(BorderFactory.createLineBorder( invalidBorderColor, 1)); } } /** * Creates and initializes the component. * Also sets oldValue for the first time. * @param manager CommandManager that will receive change commands */ @Override public JComponent getComponent(CommandManager manager) { component = createControl(); defaultBorder = component.getBorder(); oldValue = getControlValue(); this.manager = manager; return component; } /** * Reads the value of the control. * @return whatever was read from the control */ public abstract S getControlValue(); /** * Writes the value of the control. * @param newValue to write to control */ protected abstract void setControlValue(S newValue); /** * Queried within modelChanged before considering a change to * have occurred. * @return */ protected boolean changeConsideredRelevant(S oldValue, S newValue) { return ChangeFieldCommand.defaultIsChange(oldValue, newValue); } /** * Creates a Command that describes a change to the manager. * No change should be described if no change exists. * @return */ protected Command createUpdateCommand() { return new ChangeFieldCommand<S>(getControlValue(), accessor, changed); } /** * Should return whether a value is valid or not. Invalid values will * not generate updates, and will therefore not affect either model or other * views. * @param value * @return whether it is valid or not; default is "always-true" */ protected boolean isValid() { return validityConstraint.isValid(); } /** * Set validity. Should be called only from within the * @param valid */ public void refreshValid() { currentlyValid = validityConstraint.isValid(); decorateComponent(); } /** * Should be called when changes to the control are detected. * Updates oldValue after informing all interested parties. * Does nothing if new value is not valid, same as previous, * or if an update is already under way. */ protected void update() { if (isUpdating) { return; } uncontestedUpdate(getControlValue(), UpdateType.Control); } /** * Called after the control value is updated. Intended to be used by * subclasses; default implementation is to do nothing. Use to chain * updates for complex models - for example, say that field X, Y and Z * are related, so that X+Y+Z must =10. If X changes, all of them will be * invalid. When all become valid again, valueUpdated would read all * related fields and call updateValue on each of them. * * Only called if the update is valid. * * @param oldValue */ public void valueUpdated(S oldValue, S newValue) { // by default, do nothing } /** * Triggers a manual update. This should be indistinguishable from * the user typing in stuff directly (if this were a typing-enabled control) * @param nextValue value to set the control to, prior to firing an update */ public void updateValue(S nextValue) { if (isUpdating) { return; } uncontestedUpdate(nextValue, UpdateType.Synthetic); } /** * synchronizes model values with control values. Called after the control * has changed due to user (type is Control), or due to programmatic * set-to-this (type is Synthetic), or due to changed validity constraints * (type is Event). */ private void uncontestedUpdate(S nextValue, UpdateType type) { Log4jConfig.pushNDC(("" + type).charAt(0) + "@" + ("" + this.hashCode()).substring(7)); if (!isValid()) { if (currentlyValid) { // add an undoable operation to reset to the previous, valid values logger.debug("Notifying of empty command"); isUpdating = true; manager.performCommand(new EmptyCommand(changed)); isUpdating = false; } currentlyValid = false; validityConstraint.validityChanged(); logger.debug("Update invalid: {}", nextValue); // ignore - non-valid values are not written to the model } else if (!changeConsideredRelevant(oldValue, nextValue)) { if (!currentlyValid) { validityConstraint.validityChanged(); } currentlyValid = true; logger.debug("Update is nop"); // ignore - not a real update } else { // process update if (logger.isDebugEnabled()) { logger.debug("Update to {}", nextValue); } isUpdating = true; if (type.equals(UpdateType.Synthetic) || type.equals(UpdateType.Event)) { // the user did not set the control -- it needs to be set here setControlValue(nextValue); } if (!type.equals(UpdateType.Event)) { // if incoming event, then the model has already been changed manager.performCommand(createUpdateCommand()); } valueUpdated(oldValue, nextValue); oldValue = nextValue; isUpdating = false; if (!currentlyValid) { validityConstraint.validityChanged(); } currentlyValid = true; } decorateComponent(); Log4jConfig.popNDC(); } }