/******************************************************************************* * CogTool Copyright Notice and Distribution Terms * CogTool 1.3, Copyright (c) 2005-2013 Carnegie Mellon University * This software is distributed under the terms of the FSF Lesser * Gnu Public License (see LGPL.txt). * * CogTool 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 2.1 of the License, or * (at your option) any later version. * * CogTool 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 CogTool; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * CogTool makes use of several third-party components, with the * following notices: * * Eclipse SWT version 3.448 * Eclipse GEF Draw2D version 3.2.1 * * Unless otherwise indicated, all Content made available by the Eclipse * Foundation is provided to you under the terms and conditions of the Eclipse * Public License Version 1.0 ("EPL"). A copy of the EPL is provided with this * Content and is also available at http://www.eclipse.org/legal/epl-v10.html. * * CLISP version 2.38 * * Copyright (c) Sam Steingold, Bruno Haible 2001-2006 * This software is distributed under the terms of the FSF Gnu Public License. * See COPYRIGHT file in clisp installation folder for more information. * * ACT-R 6.0 * * Copyright (c) 1998-2007 Dan Bothell, Mike Byrne, Christian Lebiere & * John R Anderson. * This software is distributed under the terms of the FSF Lesser * Gnu Public License (see LGPL.txt). * * Apache Jakarta Commons-Lang 2.1 * * This product contains software developed by the Apache Software Foundation * (http://www.apache.org/) * * jopt-simple version 1.0 * * Copyright (c) 2004-2013 Paul R. Holser, Jr. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * Mozilla XULRunner 1.9.0.5 * * The contents of this file are subject to the Mozilla Public License * Version 1.1 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/. * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * * The J2SE(TM) Java Runtime Environment version 5.0 * * Copyright 2009 Sun Microsystems, Inc., 4150 * Network Circle, Santa Clara, California 95054, U.S.A. All * rights reserved. U.S. * See the LICENSE file in the jre folder for more information. ******************************************************************************/ package edu.cmu.cs.hcii.cogtool.util; import java.util.ArrayList; import java.util.EventObject; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; /** * Manages the list of undoable edits, up to a specified limit. * <p> * Modeled after the Undo Manager of Java Swing. * <p> * The default limit on the number of edits the manager will maintain is 100. * If one wishes to ignore storage considerations, one can maintain an * unlimited list by setting the limit to 0 (zero). * <p> * The list of edits is effectively divided into two: those edits * that may yet be undone, and those edits that have been undone * and that may yet be re-done. That is, the edit sequence is * divided by a "fence"; to one side are undoable edits and to the other * are re-doable edits. * <p> * All instances of <code>IUndoableEdit</code> are assumed to * support the <code>undo</code> operation. However, some may not * support the <code>redo</code> operation; if one such is "undone", * it can no longer be in the sequence of edits. Furthermore, it makes * no sense to re-do any edits that came after it in the sequence. Thus, * whenever such an edit is "undone", it and all of the subsequent edits * must be removed. * <p> * Each user interface window representing major functionality may * have its own <code>UndoManager</code>. * <p> * It is expected that the user interface will prevent attempts to * perform <code>undo</code> or <code>redo</code> when each is not possible, * respectively. Code may determine whether each operation will work * by checking for a non-<code>null</code> result from the corresponding * method (<code>editToBeUndone</code> or <code>editToBeRedone</code>). * <p> * To support this behavior, <code>UndoManager</code> implements * <code>IAlerter</code> functionality. * * @author mlh * @see javax.swing.undo.UndoManager */ public class UndoManager extends Alerter implements IUndoableEditSequence { /** * Class to reflect undo/redo events for observers */ public static class UndoRedoEvent extends EventObject { public static final int UndoAction = 0; public static final int RedoAction = 1; public static final int AddEditAction = 2; public static final int RemoveEditAction = 3; public static final int SavePointAction = 4; public static final int SetLimitAction = 5; public int actionType; public UndoRedoEvent(UndoManager mgr, int action) { super(mgr); actionType = action; } } /** * An ISaveNexus acts as a collection point for a complex model that * consists of multiple parts that may independently track undo/redo and * save-point state. */ public interface ISaveNexus { /** * Indicate that the given part is now at a save point when it * wasn't previous to this call. * * @param mgr the UndoManager representing the part of a complex model */ public void partIsNowAtSavePoint(UndoManager mgr); /** * Indicate that the given part is no longer at a save point when it * was previous to this call. * * @param mgr the UndoManager representing the part of a complex model */ public void partIsNoLongerAtSavePoint(UndoManager mgr); /** * Report whether or not the nexus is collectively at a save point. * * @return true iff the nexus is collectively at a save point */ public boolean isAtSavePoint(); } /** * Alert event that represents a change in save-point status, specifying * whether the nexus is now at a save point or is now not at a save point. */ public static class SavePointChange extends EventObject { public boolean nowAtSavePoint; public SavePointChange(UndoManager.UndoManagerNexus nexusMgr, boolean atSavePoint) { super(nexusMgr); nowAtSavePoint = atSavePoint; } } /** * UndoManagerNexus represents all of the UndoManagers for a particular * save nexus. */ protected static class UndoManagerNexus extends Alerter implements UndoManager.ISaveNexus { protected Map<Object, UndoManager> managers = new HashMap<Object, UndoManager>(); /** * The count of objects in the "modified" state since the last save * point for the nexus. */ protected int modifiedCount = 0; protected SavePointChange nowAtSavePointEvent = new SavePointChange(this, true); protected SavePointChange noLongerAtSavePointEvent = new SavePointChange(this, false); protected AlertHandler alignAllFences = new AlertHandler() { public void handleAlert(EventObject alert) { Iterator<UndoManager> mgrs = managers.values().iterator(); while (mgrs.hasNext()) { UndoManager mgr = mgrs.next(); mgr.alignFence(); } } }; /** * Indicate that the given part is now at a save point when it * wasn't previous to this call. * * @param mgr the UndoManager representing the part of a complex model */ public void partIsNowAtSavePoint(UndoManager mgr) { if (modifiedCount == 0) { throw new IllegalStateException("Decrement save point count too far."); } modifiedCount--; if (modifiedCount == 0) { raiseAlert(nowAtSavePointEvent); } } /** * Indicate that the given part is no longer at a save point when it * was previous to this call. * * @param mgr the UndoManager representing the part of a complex model */ public void partIsNoLongerAtSavePoint(UndoManager mgr) { modifiedCount++; if (modifiedCount == 1) { raiseAlert(noLongerAtSavePointEvent); } } /** * Report whether or not the nexus is collectively at a save point. * * @return true iff the nexus is collectively at a save point */ public boolean isAtSavePoint() { return (modifiedCount == 0); } /** * Propagate a save point to all managers represented by this nexus. */ public void propagateSavePoint() { Iterator<UndoManager> mgrs = managers.values().iterator(); while (mgrs.hasNext()) { UndoManager mgr = mgrs.next(); // If necessary, each of these calls will invoke // partIsNowAtSavePoint and thereby decrement modifiedCount. mgr.markSavePoint(); } } /** * Get the UndoManager associated with the given object; create a new * one if necessary. * * @param editObject the object for which to fetch an UndoManager * @param isNexusObject whether the editObject is the nexus model object * @result the UndoManager associated with the given object * @author mlh */ public UndoManager getUndoManager(Object editObject, boolean isNexusObject) { UndoManager mgr = managers.get(editObject); if (mgr == null) { // System.out.println("Making new UndoManager!"); mgr = new UndoManager(this, (! isNexusObject) && isAtSavePoint()); mgr.addHandler(this, UndoRedoEvent.class, alignAllFences); managers.put(editObject, mgr); } // System.out.println("Edits: " + mgr.edits.size()); return mgr; } /** * Recover the undo manager for the given edit object by * invoking die() on each of its edits. It is ok that there * may not be a manager for the given edit object. */ public void recoverUndoManager(Object editObject) { UndoManager mgr = managers.remove(editObject); if (mgr != null) { mgr.removeAllHandlers(this); mgr.discardAllEdits(); } } /** * Remove all edits from all undo managers for this nexus * and invoke die() on each one. */ public void recoverAllManagers() { Object[] mgrs = managers.values().toArray(); managers.clear(); for (Object mgr2 : mgrs) { UndoManager mgr = (UndoManager) mgr2; mgr.removeAllHandlers(this); mgr.discardAllEdits(); } } } /** * Keeps track of "parent" manager that collects how many objects * are in the "modified" state since the last (if any) save point. */ protected ISaveNexus saveNexus = null; protected List<IUndoableEdit> edits = new ArrayList<IUndoableEdit>(); protected int undoableEditLimit = 100; protected int undoFenceIndex = 0; protected static final int NO_SAVE_POINT = -1; /** * Index indicating that the save point. It can have one of N+2 values. * NO_SAVE_POINT indicates that the buffer reflects no save point (i.e., * any save happened at least one before or one after all edits stored * by this manager). Otherwise, the savedAtIndex must lie between 0, * indicating that the save point occurred just before all edits stored by * this manager, and edits.size() (maximally undoableEditLimit), indicating * that the save point occurred after all edits stored by this manager. */ protected int savedAtIndex = NO_SAVE_POINT; protected static Map<Object, UndoManagerNexus> nexuses = new HashMap<Object, UndoManagerNexus>(); /** * Initialize the Undo Manager, using the default limit. * * @param nexus the save nexus that is including this new manager * in its "modified" state * @param isAtSavePoint whether the manager is initially in the * "saved" or "modified" state * @author mlh */ public UndoManager(ISaveNexus nexus, boolean isAtSavePoint) { saveNexus = nexus; if (isAtSavePoint) { savedAtIndex = 0; } else if (saveNexus != null) { saveNexus.partIsNoLongerAtSavePoint(this); } } /** * Initialize the Undo Manager, using the default limit. * * @param nexus the save nexus that is including this new manager * in its "modified" state * @param isAtSavePoint whether the manager is initially in the * "saved" or "modified" state * @param undoLimit the maximum number of edits to remember * @author mlh */ public UndoManager(ISaveNexus nexus, boolean isAtSavePoint, int undoLimit) { this(nexus, isAtSavePoint); setLimit(undoLimit); } /** * Adjust the maximum number of edits the Undo Manager will maintain. * <p> * If the new limit is below the current number of edits being maintained, * the excess edits will be removed from the beginning up to the fence. (?) * Then, if the current count is still too high, (redoable) edits will be * removed from the end. * <p> * Each removed edit will have its <code>die</code> method invoked. * <p> * A zero limit means to maintain an unlimited number of edits. * <p> * The given limit must be non-negative; if negative, * an <code>IllegalArgumentException</code> exception is thrown. * <p> * When done, registered alert handlers are notified. * * @param undoLimit the maximum number of edits to remember * @exception <code>IllegalArgumentException</code> * if <code>undoLimit</code> < 0 * @author mlh */ public void setLimit(int undoLimit) { if (undoLimit >= 0) { undoableEditLimit = undoLimit; if (undoLimit > 0) { // Remove excess from ??? // Also, must handle this.savedAtIndex // Also, must handle saved-unsaved transition // ... TODO: Unimplemented raiseAlert(new UndoRedoEvent(this, UndoRedoEvent.SetLimitAction)); } } else { throw new IllegalArgumentException("Cannot set the undo limit to a negative number"); } } /** * The current limit on the number of edits this Undo Manager will * maintain. * <p> * If the return value is zero, the manager has no limit on the number * of edits it will remember. * * @return the current limit on the number of edits * @author mlh */ public int getLimit() { return undoableEditLimit; } /** * Mark the current undo buffer location as the current persisted state. */ public void markSavePoint() { // Notify nexus of unsaved-to-saved transition, if necessary if (savedAtIndex != undoFenceIndex) { savedAtIndex = undoFenceIndex; if (saveNexus != null) { saveNexus.partIsNowAtSavePoint(this); } raiseAlert(new UndoRedoEvent(this, UndoRedoEvent.SavePointAction)); } } /** * Checks whether the current undo buffer location is the same as the * current persisted state. * @return true iff the undo buffer is at the last marked save point */ public boolean isSavePoint() { return (undoFenceIndex == savedAtIndex); } /** * Internal support method: removes all edits beyond the fence. * * @author mlh */ protected void trimEdits() { int editCount = edits.size(); if (undoFenceIndex < editCount) { // Note: This works even if savedAtIndex is NO_SAVE_POINT (i.e. -1) if (savedAtIndex > undoFenceIndex) { savedAtIndex = NO_SAVE_POINT; } ListIterator<IUndoableEdit> editsInReverse = edits.listIterator(editCount); while ((undoFenceIndex < editCount) && editsInReverse.hasPrevious()) { IUndoableEdit edit = editsInReverse.previous(); edit.die(); editsInReverse.remove(); editCount--; } } } /** * Add a new undoable edit to the sequence being maintained. * <p> * All re-doable edits are removed before adding the new edit, * since it no longer makes sense to redo them in the context * of the change just added (and presumably performed). * <p> * If the number of edits is limited and the resulting number * of edits is greater than the limit, the oldest undoable edit * is removed. (Recall, there will be no re-doable edits!) * <p> * Each removed edit will have its <code>die</code> method invoked. * <p> * When done, registered alert handlers are notified. * * @param newEdit the edit to add to the end of the sequence * @return <code>true</code> iff the add was successful * @author mlh */ public boolean addEdit(IUndoableEdit newEdit) { // System.out.println("Adding undoable edit: " + newEdit.getUndoPresentationName()); // Remove redoable edits that come after the fence // Do this before adding to avoid O(n) insert trimEdits(); edits.add(undoFenceIndex, newEdit); if (newEdit.getManager() == null) { newEdit.setManager(this); } if ((saveNexus != null) && (undoFenceIndex == savedAtIndex)) { saveNexus.partIsNoLongerAtSavePoint(this); } undoFenceIndex++; // If larger than the limit, remove the oldest edit if ((undoableEditLimit > 0) && (edits.size() > undoableEditLimit)) { IUndoableEdit excessEdit = edits.get(0); excessEdit.die(); // saveAtZero is always false if the start of the buffer is gone edits.remove(0); // Note: This works even if savedAtIndex is zero // since NO_SAVE_POINT is -1! if (savedAtIndex != NO_SAVE_POINT) { savedAtIndex--; } undoFenceIndex--; } raiseAlert(new UndoRedoEvent(this, UndoRedoEvent.AddEditAction)); return true; } // addEdit /** * Ascertain the index of the edit that is next to be re-done; * returns -1 if no candidate edit. */ protected int editIndexToBeRedone() { int editIndex = undoFenceIndex; int numEdits = edits.size(); while (editIndex < numEdits) { IUndoableEdit edit = edits.get(editIndex); UndoManager mgr = edit.getManager(); // Compare in this order or suffer an infinite loop! if ((mgr == this) || (edit == mgr.editToBeRedone())) { return editIndex; } editIndex++; } return -1; } /** * Return whether there is an edit to be redone. */ public boolean hasEditToBeRedone() { return editIndexToBeRedone() != -1; } /** * Ascertain the edit being maintained by the manager that is next * to be re-done, if one exists. * <p> * One can determine if the <code>redo</code> method will succeed * if this method returns a non-<code>null</code> instance. * * @return the next edit to be re-done, if one exists; * <code>null</code> otherwise. * @exception <i>none</i> * @author mlh */ public IUndoableEdit editToBeRedone() { int editIndex = editIndexToBeRedone(); return (editIndex != -1) ? edits.get(editIndex) : null; } /** * Ascertain the index of the edit that is next to be undone; * returns -1 if no candidate edit. */ protected int editIndexToBeUndone() { int editIndex = undoFenceIndex; while (editIndex-- > 0) { IUndoableEdit edit = edits.get(editIndex); UndoManager mgr = edit.getManager(); // Compare in this order or suffer an infinite loop! if ((mgr == this) || (edit == mgr.editToBeUndone())) { return editIndex; } } return -1; } /** * Return whether there is an edit to be undone. */ public boolean hasEditToBeUndone() { return editIndexToBeUndone() != -1; } /** * Ascertain the edit being maintained by the manager that is next * to be undone, if one exists. * <p> * One can determine if the <code>undo</code> method will succeed * if this method returns a non-<code>null</code> instance. * * @return the next edit to be undone, if one exists; * <code>null</code> otherwise * @exception <i>none</i> * @author mlh */ public IUndoableEdit editToBeUndone() { int editIndex = editIndexToBeUndone(); return (editIndex != -1) ? edits.get(editIndex) : null; } /** * Fetch the presentation name of the next edit to be re-done. * <p> * If there is no next edit to be re-done, a generic description * is returned. * * @return the presentation description of the next edit to * be undone, if one exists; the result of * <code>L10N.get("UNDO.Redo", "Redo")</code> otherwise * @author mlh */ public String getRedoPresentationName() { IUndoableEdit nextRedo = editToBeRedone(); if (nextRedo != null) { return nextRedo.getRedoPresentationName(); } return L10N.get("UNDO.Redo", "Redo"); } /** * Fetch the presentation name of the next edit to be undone. * <p> * If there is no next edit to be undone, a generic description * is returned. * * @return the presentation description of the next edit to * be undone, if one exists; the result of * <code>L10N.get("UNDO.Undo", "Undo")</code> otherwise * @author mlh */ public String getUndoPresentationName() { IUndoableEdit nextUndo = editToBeUndone(); if (nextUndo != null) { return nextUndo.getUndoPresentationName(); } return L10N.get("UNDO.Undo", "Undo"); } /** * Remove all edits from the manager. * <p> * All re-doable edits are removed in reverse order. * Each removed edit will have its <code>die</code> method invoked. * <p> * When done, registered alert handlers are notified. * * @author mlh */ public void discardAllEdits() { undoFenceIndex = 0; trimEdits(); raiseAlert(new UndoRedoEvent(this, UndoRedoEvent.RemoveEditAction)); } /** * Update the fence index by the given delta, updating whether or not * this UndoManager is at its save point. */ protected void updateFence(int fenceDelta) { if (saveNexus != null) { if (savedAtIndex == undoFenceIndex) { saveNexus.partIsNoLongerAtSavePoint(this); } else if (savedAtIndex == (undoFenceIndex + fenceDelta)) { saveNexus.partIsNowAtSavePoint(this); } } undoFenceIndex += fenceDelta; } /** * Perform the next re-doable edit being maintained by the manager. * <p> * If there is no next edit to be re-done, a * <code>CannotRedoException</code> exception is thrown. * <p> * When done, registered alert handlers are notified. * * @exception <code>CannotRedoException</code> * if no re-doable edit exists in the manager's list; * also, the edit may also throw this exception * @author mlh */ public void redo() { int editIndex = editIndexToBeRedone(); if (editIndex == -1) { throw new CannotRedoException(); } IUndoableEdit nextRedo = edits.get(editIndex); nextRedo.redo(); updateFence(editIndex - undoFenceIndex + 1); raiseAlert(new UndoRedoEvent(this, UndoRedoEvent.RedoAction)); } /** * Undo according to the next undoable edit being maintained by the * manager. * <p> * If there is no next edit to be undone, a * <code>CannotUndoException</code> exception is thrown. * <p> * If the edit is not re-doable, all edits on the "redo" side * of the fence are removed in reverse order. * <p> * Each removed edit will have its <code>die</code> method invoked. * <p> * When done, registered alert handlers are notified. * * @exception <code>CannotUndoException</code> * if no undoable edit exists in the manager's list; * also, the edit may also throw this exception * @author mlh */ public void undo() { int editIndex = editIndexToBeUndone(); if (editIndex == -1) { throw new CannotUndoException(); } IUndoableEdit nextUndo = edits.get(editIndex); nextUndo.undo(); updateFence(editIndex - undoFenceIndex); // If can't redo, remove redoable edits that come after the fence if (! nextUndo.canRedo()) { trimEdits(); } raiseAlert(new UndoRedoEvent(this, UndoRedoEvent.UndoAction)); } /** * Determine how far to adjust the fence index past edits toward * the redo side that have been done. * * Assumes this.edits.size() > 0 && this.undoFenceIndex < this.edits.size() */ protected void alignFenceRedo() { int numEdits = edits.size(); int fenceIndex = undoFenceIndex; IUndoableEdit edit = edits.get(fenceIndex); while (edit.isDone()) { if (++fenceIndex == numEdits) { break; } edit = edits.get(fenceIndex); } if (fenceIndex != undoFenceIndex) { updateFence(fenceIndex - undoFenceIndex); } } /** * Determine how far to adjust the fence index past edits toward * the undo side that have been undone. * * Assumes this.edits.size() > 0 && this.undoFenceIndex > 0 */ protected void alignFenceUndo() { int fenceIndex = undoFenceIndex; IUndoableEdit edit = edits.get(fenceIndex - 1); while (! edit.isDone()) { if (--fenceIndex == 0) { break; } edit = edits.get(fenceIndex - 1); } if (fenceIndex != undoFenceIndex) { updateFence(fenceIndex - undoFenceIndex); } } /** * Adjust the fence index in case an unowned edit is at the fence * and has been undone or redone. */ public void alignFence() { int numEdits = edits.size(); if (numEdits > 0) { // If at the end of the edit list, check in the undo direction if (undoFenceIndex == numEdits) { alignFenceUndo(); } // If at the start of the edit list, check in the redo direction else if (undoFenceIndex == 0) { alignFenceRedo(); } else { // In the middle somewhere IUndoableEdit redoEdit = edits.get(undoFenceIndex); // If the next redo edit has been done, then adjust // in the redo direction. if (redoEdit.isDone()) { alignFenceRedo(); } else { // Not done, so check in the undo direction instead. alignFenceUndo(); } } } } /** * Recover the UndoManager for the given object, which is a component part * of a more complex model object. * * @param editObject the object for which to fetch an UndoManager * @param nexusObject the complex object containing the editObject * @throws IllegalStateException if no UndoManagerNexus exists * for the given nexusObject * @author mlh */ public static void recoverUndoManager(Object editObject, Object nexusObject) { UndoManagerNexus nexus = nexuses.get(nexusObject); if (nexus == null) { throw new IllegalStateException("Complex model has no UndoManagerNexus"); } nexus.recoverUndoManager(editObject); } /** * When the nexus model object is no longer being edited, it may be * necessary to eliminate the associated UndoManager instances. * * @param nexusObject the object for which to recover UndoManager instances * @author mlh */ public static void recoverUndoManagers(Object nexusObject) { UndoManagerNexus nexus = nexuses.get(nexusObject); if (nexus != null) { nexus.recoverAllManagers(); nexuses.remove(nexusObject); } } /** * Report whether or not the nexus for the given complex model object * is collectively at a save point. * * @return true iff the nexus is collectively at a save point */ public static boolean isAtSavePoint(Object nexusObject) { UndoManagerNexus nexus = nexuses.get(nexusObject); if (nexus == null) { throw new IllegalStateException("Complex model has no UndoManagerNexus"); } return nexus.isAtSavePoint(); } /** * Mark a save point for a complex model object and all of its * component parts that may have UndoManager instances. * * @param nexusObject the object for which to mark UndoManager instances * as saved. * @throws IllegalStateException if no UndoManagerNexus exists * for the given nexusObject * @author mlh */ public static void markSavePoint(Object nexusObject) { UndoManagerNexus nexus = nexuses.get(nexusObject); if (nexus == null) { throw new IllegalStateException("Complex model has no UndoManagerNexus"); } nexus.propagateSavePoint(); } /** * Remove alert handler for dealing with save point change transitions. * * @param nexusObject the complex model object acting as a nexus * @param handler the alert handler to remove * @author mlh */ public static void removeSavePointChangeHandler(Object nexusObject, AlertHandler handler) { UndoManagerNexus nexus = nexuses.get(nexusObject); if (nexus == null) { throw new IllegalStateException("Complex model has no UndoManagerNexus"); } nexus.removeHandler(SavePointChange.class, handler); } /** * Add alert handler for dealing with save point change transitions. * * @param nexusObject the complex model object acting as a nexus * @param handler the alert handler to add * @author mlh */ public static void addSavePointChangeHandler(Object nexusObject, AlertHandler handler) { UndoManagerNexus nexus = nexuses.get(nexusObject); if (nexus == null) { throw new IllegalStateException("Complex model has no UndoManagerNexus"); } nexus.addHandler(null, SavePointChange.class, handler); } /** * Get the UndoManager for the given object, which is a component part * of a more complex model object. * * @param editObject the object for which to fetch an UndoManager * @param nexusObject the complex object containing the editObject * @result the UndoManager associated with the given object * @throws IllegalStateException if no UndoManagerNexus exists * for the given nexusObject * @author mlh */ public static UndoManager getUndoManager(Object editObject, Object nexusObject) { UndoManagerNexus nexus = nexuses.get(nexusObject); if (nexus == null) { throw new IllegalStateException("Complex model has no UndoManagerNexus"); } return nexus.getUndoManager(editObject, false); } /** * Get the UndoManager for the given object, which will also act as a * nexus (i.e., complex model) for component parts that will have * independent UndoManager instances. * * @param nexusObject the object for which to fetch an UndoManager * @result the UndoManager associated with the given object * @author mlh */ public static UndoManager getUndoManager(Object nexusObject) { UndoManagerNexus nexus = nexuses.get(nexusObject); if (nexus == null) { nexus = new UndoManagerNexus(); nexuses.put(nexusObject, nexus); } return nexus.getUndoManager(nexusObject, true); } }