/** * */ package cz.cuni.mff.peckam.java.origamist.model; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Hashtable; import java.util.Locale; import javax.xml.bind.annotation.XmlTransient; import cz.cuni.mff.peckam.java.origamist.common.LangString; import cz.cuni.mff.peckam.java.origamist.exceptions.InvalidOperationException; import cz.cuni.mff.peckam.java.origamist.exceptions.PaperStructureException; import cz.cuni.mff.peckam.java.origamist.modelstate.ModelState; import cz.cuni.mff.peckam.java.origamist.utils.ChangeNotification; import cz.cuni.mff.peckam.java.origamist.utils.LangStringHashtableObserver; import cz.cuni.mff.peckam.java.origamist.utils.ObservableList; import cz.cuni.mff.peckam.java.origamist.utils.ObservablePropertyEvent; import cz.cuni.mff.peckam.java.origamist.utils.ObservablePropertyListener; import cz.cuni.mff.peckam.java.origamist.utils.Observer; /** * A step of the model creation. * <p> * A step is a group of operations displayed at once. * <p> * Provides property: previous * <p> * Provides property: next * <p> * Provides property: steps * * @author Martin Pecka */ @XmlTransient public class Step extends cz.cuni.mff.peckam.java.origamist.model.jaxb.Step { /** The previous property. */ public static final String PREVIOUS_PROPERTY = "previous:cz.cuni.mff.peckam.java.origamist.model.Step"; /** The next property. */ public static final String NEXT_PROPERTY = "next:cz.cuni.mff.peckam.java.origamist.model.Step"; /** * The hastable for more comfortable search in localized descriptions. */ protected final Hashtable<Locale, String> descriptions = new Hashtable<Locale, String>(); /** * The cached model state after performing this step (index 0 without delayed operations, index 1 with them). */ protected ModelState[] modelStates = new ModelState[2]; /** * If this is the first step, use this model state as the previous one. */ protected ModelState defaultModelState = null; /** The exception that was thrown by an operation of this step. */ protected RuntimeException thrownException = null; /** * The step preceeding this one. If this is the first one, previous is null. */ protected Step previous = null; /** * The step succeeding this one. If this is the last one, next is null. */ protected Step next = null; /** The {@link Steps} object this step is part of. */ protected Steps steps = null; /** * Create a new step. */ public Step() { setId(1); // the default step id ((ObservableList<LangString>) getDescription()).addObserver(new LangStringHashtableObserver(descriptions)); addPropertyChangeListener(IMAGE_PROPERTY, new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { invalidateThisModelState(); } }); ((ObservableList<Operation>) getOperations()).addObserver(new Observer<Operation>() { @Override public void changePerformed(ChangeNotification<? extends Operation> change) { invalidateModelState(); } }); addPrefixedObservablePropertyListener(new ObservablePropertyListener<Operation>() { @Override public void changePerformed(ObservablePropertyEvent<? extends Operation> evt) { invalidateModelState(); } }, OPERATIONS_PROPERTY); addPrefixedPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { invalidateModelState(); } }, OPERATIONS_PROPERTY); if (zoom == null) zoom = 100.0; } /** * Return the localized description of the step. * * @param l The locale of the description. * @return The localized description or null if l is null or not found */ public String getDescription(Locale l) { if (descriptions.size() == 0) { // here we really don't want to display a not found message; // instead, we signalize that no message is defined, so its // space may be used in another way return null; } if (l == null || !descriptions.containsKey(l)) return descriptions.elements().nextElement(); return descriptions.get(l); } /** * Return the localized description of the step with the number of this step prepended. * * @param l The locale of the description. * @return The localized description or null if l is null or not found */ public String getDescriptionWithId(Locale l) { String result = getDescription(l); if (result == null) return null; return getId() + ") " + result; } /** * Add a description in the given locale. * * @param l The locale of the description * @param name The description to add */ public void addDescription(Locale l, String desc) { LangString s = (LangString) new cz.cuni.mff.peckam.java.origamist.common.jaxb.ObjectFactory() .createLangString(); s.setLang(l); s.setValue(desc); this.description.add(s); } /** * Perform folding from the previous step's state to a new state by this step. * <p> * If this method throws an exception, it will continue to throw it until a call to * {@link #invalidateThisModelState()} is performed. * * @param withDelayed Whether to get the model state with or without delayed operations. * @return The state the model would have after performing this step. * * @throws InvalidOperationException If an operation cannot be done. * @throws PaperStructureException If the paper should have been torn or intersected. */ public synchronized ModelState getModelState(boolean withDelayed) throws InvalidOperationException, PaperStructureException { int i = withDelayed ? 1 : 0; if (thrownException != null) throw thrownException; if (this.modelStates[i] != null) return this.modelStates[i]; if (previous != null) { this.modelStates[i] = previous.getModelState(true).clone(); } else { if (this.defaultModelState == null) { throw new IllegalStateException("Cannot create the default model state for step " + getId()); } this.modelStates[i] = this.defaultModelState.clone(); } this.modelStates[i].proceedToNextStep(); this.modelStates[i].setStep(this); if (operations == null) return this.modelStates[i]; try { for (Operation o : operations) { if (withDelayed || !o.isCompletelyDelayedToNextStep()) { try { this.modelStates[i] = o.getModelState(this.modelStates[i]); } catch (InvalidOperationException e) { if (e.getOperation() == null) e.setOperation(o); throw e; } } } this.modelStates[i].checkPaperPhysicalConstraints(); } catch (RuntimeException e) { thrownException = e; this.modelStates[i] = null; throw e; } if (!withDelayed) this.modelStates[i].revertDelayedOperations(); return this.modelStates[i]; } /** * @param previous the previous step */ public void setPrevious(Step previous) { Step old = this.previous; this.previous = previous; if ((old != previous && (old == null || previous == null)) || (old != null && !old.equals(previous))) { support.firePropertyChange(PREVIOUS_PROPERTY, old, previous); invalidateModelState(); } } /** * @param next the next step */ public void setNext(Step next) { Step old = this.next; this.next = next; if ((old != next && (old == null || next == null)) || (old != null && !old.equals(next))) { support.firePropertyChange(NEXT_PROPERTY, old, next); invalidateModelState(); } } /** * @return the previous */ @XmlTransient public Step getPrevious() { return previous; } /** * @return the next */ @XmlTransient public Step getNext() { return next; } /** * @param modelState The model state to be used as the previous for the first step. Has no meaning for other steps * than the first one. */ public void setDefaultModelState(ModelState modelState) { ModelState oldState = this.defaultModelState; this.defaultModelState = modelState; if (oldState != modelState) invalidateModelState(); } /** * Causes this step (and all following steps) to recompute its modelState from the previous step. */ public void invalidateModelState() { invalidateThisModelState(); if (this.next != null) this.next.invalidateModelState(); } /** * Invalidate only this step's model state. You should call this only if you are sure that this model state's change * won't affect any further steps (eg. if you change the image in the step). */ protected void invalidateThisModelState() { this.modelStates[0] = null; this.modelStates[1] = null; this.thrownException = null; } /** * Free all the memory held by model state information. */ public void unloadModelState() { invalidateThisModelState(); } /** * Check if the model state is valid. * * @param withDelayed Whether to check the model state with or without delayed operations. * @return True if the model state is still valid. */ public boolean isModelStateValid(boolean withDelayed) { return (!withDelayed && modelStates[0] != null) || (withDelayed && modelStates[1] != null); } @Override public void setColspan(Integer value) { if (!value.equals(1)) super.setColspan(value); else super.setColspan(null); } @Override public void setRowspan(Integer value) { if (!value.equals(1)) super.setRowspan(value); else super.setRowspan(null); } /** * @return True if this step can be edited. */ public boolean isEditable() { return next == null; } @Override protected String[] getNonChildProperties() { return new String[] { PREVIOUS_PROPERTY, NEXT_PROPERTY }; } }