You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2026/04/19 23:57:52 UTC

(cayenne) 04/06: Modeler events cleanup

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit a55dd40ed9ec1cfb373a70228941f0362a2de750
Author: Andrus Adamchik <an...@objectstyle.com>
AuthorDate: Sun Apr 19 11:54:36 2026 -0400

    Modeler events cleanup
    
    1. smarter history tracking
---
 .../apache/cayenne/modeler/NavigationHistory.java  | 167 +++++++++
 .../apache/cayenne/modeler/ProjectController.java  | 299 +++-------------
 .../apache/cayenne/modeler/util/CircularArray.java | 398 ---------------------
 .../cayenne/modeler/util/CircularArrayTest.java    | 143 --------
 4 files changed, 222 insertions(+), 785 deletions(-)

diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/NavigationHistory.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/NavigationHistory.java
new file mode 100644
index 000000000..e9268b2b6
--- /dev/null
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/NavigationHistory.java
@@ -0,0 +1,167 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.modeler;
+
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.event.EmbeddableEvent;
+import org.apache.cayenne.map.event.EntityEvent;
+import org.apache.cayenne.modeler.event.display.DataMapDisplayEvent;
+import org.apache.cayenne.modeler.event.display.DataNodeDisplayEvent;
+import org.apache.cayenne.modeler.event.display.DisplayEvent;
+import org.apache.cayenne.modeler.event.display.DomainDisplayEvent;
+import org.apache.cayenne.modeler.event.display.EmbeddableDisplayEvent;
+import org.apache.cayenne.modeler.event.display.EntityDisplayEvent;
+import org.apache.cayenne.modeler.event.display.ProcedureDisplayEvent;
+import org.apache.cayenne.modeler.event.display.QueryDisplayEvent;
+import org.apache.cayenne.modeler.event.model.DataMapEvent;
+import org.apache.cayenne.modeler.event.model.DataNodeEvent;
+import org.apache.cayenne.modeler.event.model.DomainEvent;
+import org.apache.cayenne.modeler.event.model.ProcedureEvent;
+import org.apache.cayenne.modeler.event.model.QueryEvent;
+
+import java.util.ArrayList;
+import java.util.EventObject;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Consumer;
+
+class NavigationHistory {
+    private final static int MAX_HISTORY_SIZE = 20;
+
+    private final List<DisplayEvent> history;
+    private final List<DisplayEvent> replayHistory;
+    private DisplayEvent currentEvent;
+
+    public NavigationHistory() {
+        this.history = new ArrayList<>(MAX_HISTORY_SIZE);
+        this.replayHistory = new ArrayList<>(MAX_HISTORY_SIZE);
+    }
+
+    public DisplayEvent getLastEvent() {
+        return history.isEmpty() ? null : history.get(history.size() - 1);
+    }
+
+    public void recordEvent(DisplayEvent e) {
+        recordEvent(history, e);
+    }
+
+    public void replayLastEvent(ProjectController controller) {
+
+        if (history.isEmpty()) {
+            return;
+        }
+
+        DisplayEvent e = history.remove(history.size() - 1);
+        recordEvent(replayHistory, e);
+        replay(controller, e);
+    }
+
+
+    public void replayNextEvent(ProjectController controller) {
+        if (replayHistory.isEmpty()) {
+            return;
+        }
+
+        DisplayEvent e = replayHistory.remove(replayHistory.size() - 1);
+        recordEvent(history, e);
+        replay(controller, e);
+    }
+
+    private void recordEvent(List<DisplayEvent> target, DisplayEvent e) {
+
+        // "currentEvent" will be the same as "e" if this was caused by replay
+        if (this.currentEvent != null && this.currentEvent != e) {
+            if (target.size() >= MAX_HISTORY_SIZE) {
+                target.remove(0);
+            }
+
+            target.add(this.currentEvent);
+        }
+
+        this.currentEvent = e;
+    }
+
+    private void replay(ProjectController controller, DisplayEvent e) {
+        if (e instanceof EntityDisplayEvent) {
+            EntityDisplayEvent ede = (EntityDisplayEvent) e;
+            if (ede.getEntity() instanceof ObjEntity) {
+                controller.fireObjEntitySelected(ede);
+            } else if (ede.getEntity() instanceof DbEntity) {
+                controller.fireDbEntitySelected(ede);
+            }
+        } else if (e instanceof EmbeddableDisplayEvent) {
+            controller.fireEmbeddableSelected((EmbeddableDisplayEvent) e);
+        } else if (e instanceof ProcedureDisplayEvent) {
+            controller.fireProcedureSelected((ProcedureDisplayEvent) e);
+        } else if (e instanceof QueryDisplayEvent) {
+            controller.fireQuerySelected((QueryDisplayEvent) e);
+        } else if (e instanceof DataMapDisplayEvent) {
+            controller.fireDataMapSelected((DataMapDisplayEvent) e);
+        } else if (e instanceof DataNodeDisplayEvent) {
+            controller.fireDataNodeSelected((DataNodeDisplayEvent) e);
+        } else if (e instanceof DomainDisplayEvent) {
+            controller.fireDomainSelected((DomainDisplayEvent) e);
+        }
+    }
+
+    public void forgetObject(EventObject e) {
+
+        Consumer<List<DisplayEvent>> remover = list -> {
+            Iterator<DisplayEvent> it = list.iterator();
+            while (it.hasNext()) {
+                DisplayEvent de = it.next();
+                if (e instanceof EntityEvent && de instanceof EntityDisplayEvent) {
+                    if (((EntityEvent) e).getEntity() == ((EntityDisplayEvent) de).getEntity()) {
+                        it.remove();
+                    }
+                } else if (e instanceof EmbeddableEvent && de instanceof EmbeddableDisplayEvent) {
+                    if (((EmbeddableEvent) e).getEmbeddable() == ((EmbeddableDisplayEvent) de).getEmbeddable()) {
+                        it.remove();
+                    }
+                } else if (e instanceof ProcedureEvent && de instanceof ProcedureDisplayEvent) {
+                    if (((ProcedureEvent) e).getProcedure() == ((ProcedureDisplayEvent) de).getProcedure()) {
+                        it.remove();
+                    }
+                } else if (e instanceof QueryEvent && de instanceof QueryDisplayEvent) {
+                    if (((QueryEvent) e).getQuery() == ((QueryDisplayEvent) de).getQuery()) {
+                        it.remove();
+                    }
+                } else if (e instanceof DataMapEvent && de instanceof DataMapDisplayEvent) {
+                    if (((DataMapEvent) e).getDataMap() == ((DataMapDisplayEvent) de).getDataMap()) {
+                        it.remove();
+                    }
+                } else if (e instanceof DataNodeEvent && de instanceof DataNodeDisplayEvent) {
+                    if (((DataNodeEvent) e).getDataNode() == ((DataNodeDisplayEvent) de).getDataNode()) {
+                        it.remove();
+                    }
+                } else if (e instanceof DomainEvent && de instanceof DomainDisplayEvent) {
+                    if (((DomainEvent) e).getDomain() == ((DomainDisplayEvent) de).getDomain()) {
+                        it.remove();
+                    }
+                }
+            }
+        };
+
+        remover.accept(history);
+        remover.accept(replayHistory);
+    }
+
+}
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ProjectController.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ProjectController.java
index be4c3c33b..ea57d7d06 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ProjectController.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/ProjectController.java
@@ -23,6 +23,8 @@ import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.configuration.ConfigurationNode;
 import org.apache.cayenne.configuration.DataChannelDescriptor;
 import org.apache.cayenne.configuration.DataNodeDescriptor;
+import org.apache.cayenne.map.Attribute;
+import org.apache.cayenne.map.Relationship;
 import org.apache.cayenne.modeler.event.display.*;
 import org.apache.cayenne.modeler.event.model.*;
 import org.apache.cayenne.map.DataMap;
@@ -61,7 +63,6 @@ import org.apache.cayenne.modeler.pref.DataMapDefaults;
 import org.apache.cayenne.modeler.pref.DataNodeDefaults;
 import org.apache.cayenne.modeler.pref.ProjectStatePreferences;
 import org.apache.cayenne.modeler.util.CayenneController;
-import org.apache.cayenne.modeler.util.CircularArray;
 import org.apache.cayenne.modeler.util.Comparators;
 import org.apache.cayenne.project.ConfigurationNodeParentGetter;
 import org.apache.cayenne.project.Project;
@@ -75,7 +76,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.EventListener;
-import java.util.EventObject;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -89,14 +89,14 @@ public class ProjectController extends CayenneController {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(ProjectController.class);
 
-    private final static int MAX_HISTORY_SIZE = 20;
 
+    private NavigationHistory history;
     private EventListenerList listeners;
     private boolean dirty;
     private Project project;
     private Preferences projectControllerPreferences;
     private ControllerState state;
-    private final CircularArray<ControllerState> controllerStateHistory;
+
 
     private EntityResolver entityResolver;
     private ProjectFileChangeTracker fileChangeTracker;
@@ -104,8 +104,8 @@ public class ProjectController extends CayenneController {
     public ProjectController(CayenneModelerController parent) {
         super(parent);
         this.listeners = new EventListenerList();
-        this.controllerStateHistory = new CircularArray<>(MAX_HISTORY_SIZE);
         this.state = new ControllerState();
+        this.history = new NavigationHistory();
     }
 
     @Override
@@ -292,77 +292,17 @@ public class ProjectController extends CayenneController {
     }
 
     public void reset() {
-        clearState();
+        state = new ControllerState();
         setDirty(false);
         listeners = new EventListenerList();
-        controllerStateHistory.clear();
+        history = new NavigationHistory();
     }
 
     public boolean isDirty() {
         return dirty;
     }
 
-    /**
-     * Resets all current models to null.
-     */
-    private void clearState() {
-        state = new ControllerState();
-    }
-
-    private void saveState(DisplayEvent e) {
-        if (!controllerStateHistory.contains(state)) {
-            state.event = e;
-            controllerStateHistory.add(state);
-        }
-    }
 
-    private void removeFromHistory(EventObject e) {
-
-        int count = controllerStateHistory.size();
-        List<ControllerState> removeList = new ArrayList<>();
-
-        for (int i = 0; i < count; i++) {
-            ControllerState cs = controllerStateHistory.get(i);
-            if (cs == null || cs.event == null) {
-                continue;
-            }
-            EventObject csEvent = cs.event;
-
-            if (e instanceof EntityEvent && csEvent instanceof EntityDisplayEvent) {
-                if (((EntityEvent) e).getEntity() == ((EntityDisplayEvent) csEvent).getEntity()) {
-                    removeList.add(cs);
-                }
-            } else if (e instanceof EmbeddableEvent && csEvent instanceof EmbeddableDisplayEvent) {
-                if (((EmbeddableEvent) e).getEmbeddable() == ((EmbeddableDisplayEvent) csEvent).getEmbeddable()) {
-                    removeList.add(cs);
-                }
-            } else if (e instanceof ProcedureEvent && csEvent instanceof ProcedureDisplayEvent) {
-                if (((ProcedureEvent) e).getProcedure() == ((ProcedureDisplayEvent) csEvent).getProcedure()) {
-                    removeList.add(cs);
-                }
-            } else if (e instanceof QueryEvent && csEvent instanceof QueryDisplayEvent) {
-                if (((QueryEvent) e).getQuery() == ((QueryDisplayEvent) csEvent).getQuery()) {
-                    removeList.add(cs);
-                }
-            } else if (e instanceof DataMapEvent && csEvent instanceof DataMapDisplayEvent) {
-                if (((DataMapEvent) e).getDataMap() == ((DataMapDisplayEvent) csEvent).getDataMap()) {
-                    removeList.add(cs);
-                }
-            } else if (e instanceof DataNodeEvent && csEvent instanceof DataNodeDisplayEvent) {
-                if (((DataNodeEvent) e).getDataNode() == ((DataNodeDisplayEvent) csEvent).getDataNode()) {
-                    removeList.add(cs);
-                }
-            } else if (e instanceof DomainEvent && csEvent instanceof DomainDisplayEvent) {
-                if (((DomainEvent) e).getDomain() == ((DomainDisplayEvent) csEvent).getDomain()) {
-                    removeList.add(cs);
-                }
-            }
-        }
-
-        for (ControllerState o : removeList) {
-            controllerStateHistory.remove(o);
-        }
-    }
 
     public DataChannelDescriptor getSelectedDataDomain() {
         return state.dataDomain;
@@ -430,7 +370,7 @@ public class ProjectController extends CayenneController {
     }
 
     public DisplayEvent getLastDisplayEvent() {
-        return state.event;
+        return history.getLastEvent();
     }
 
     public void addDomainDisplayListener(DomainDisplayListener listener) {
@@ -595,9 +535,9 @@ public class ProjectController extends CayenneController {
         if (e.getDomain() != state.dataDomain || (
                 state.dataNode != null || state.dataMap != null || state.dbEntity != null || state.objEntity != null || state.procedure != null || state.query != null || state.embeddable != null
         )) {
-            clearState();
+            state = new ControllerState();
             state.dataDomain = e.getDomain();
-            saveState(e);
+            history.recordEvent(e);
         }
 
         for (DomainDisplayListener listener : listeners.getListeners(DomainDisplayListener.class)) {
@@ -622,7 +562,7 @@ public class ProjectController extends CayenneController {
         setDirty(true);
 
         if (e.getId() == MapEvent.REMOVE) {
-            removeFromHistory(e);
+            history.forgetObject(e);
         }
 
         for (DomainListener listener : listeners.getListeners(DomainListener.class)) {
@@ -642,10 +582,10 @@ public class ProjectController extends CayenneController {
         if (e.getDataNode() != state.dataNode || (
                 state.dataMap != null || state.dbEntity != null || state.objEntity != null || state.procedure != null || state.query != null || state.embeddable != null
         )) {
-            clearState();
+            state = new ControllerState();
             state.dataDomain = e.getDomain();
             state.dataNode = e.getDataNode();
-            saveState(e);
+            history.recordEvent(e);
         }
 
         for (DataNodeDisplayListener listener : listeners.getListeners(DataNodeDisplayListener.class)) {
@@ -658,7 +598,7 @@ public class ProjectController extends CayenneController {
         setDirty(true);
 
         if (e.getId() == MapEvent.REMOVE) {
-            removeFromHistory(e);
+            history.forgetObject(e);
         }
 
         for (DataNodeListener listener : listeners.getListeners(DataNodeListener.class)) {
@@ -683,12 +623,12 @@ public class ProjectController extends CayenneController {
         if (e.getDataMap() != state.dataMap || (
                 state.dbEntity != null || state.objEntity != null || state.procedure != null || state.query != null || state.embeddable != null
         )) {
-            clearState();
+            state = new ControllerState();
             state.dataDomain = e.getDomain();
             state.dataNode = e.getDataNode();
             state.dataMap = e.getDataMap();
 
-            saveState(e);
+            history.recordEvent(e);
         }
 
         for (DataMapDisplayListener listener : listeners.getListeners(DataMapDisplayListener.class)) {
@@ -712,7 +652,7 @@ public class ProjectController extends CayenneController {
         setDirty(true);
 
         if (e.getId() == MapEvent.REMOVE) {
-            removeFromHistory(e);
+            history.forgetObject(e);
         }
 
         for (DataMapListener eventListener : listeners.getListeners(DataMapListener.class)) {
@@ -741,7 +681,7 @@ public class ProjectController extends CayenneController {
         }
 
         if (e.getId() == MapEvent.REMOVE) {
-            removeFromHistory(e);
+            history.forgetObject(e);
         }
 
         for (ObjEntityListener listener : listeners.getListeners(ObjEntityListener.class)) {
@@ -770,7 +710,7 @@ public class ProjectController extends CayenneController {
         }
 
         if (e.getId() == MapEvent.REMOVE) {
-            removeFromHistory(e);
+            history.forgetObject(e);
         }
 
         for (DbEntityListener listener : listeners.getListeners(DbEntityListener.class)) {
@@ -795,7 +735,7 @@ public class ProjectController extends CayenneController {
         setDirty(true);
 
         if (e.getId() == MapEvent.REMOVE) {
-            removeFromHistory(e);
+            history.forgetObject(e);
         }
 
         for (QueryListener eventListener : listeners.getListeners(QueryListener.class)) {
@@ -820,7 +760,7 @@ public class ProjectController extends CayenneController {
         setDirty(true);
 
         if (e.getId() == MapEvent.REMOVE) {
-            removeFromHistory(e);
+            history.forgetObject(e);
         }
 
         for (ProcedureListener eventListener : listeners.getListeners(ProcedureListener.class)) {
@@ -867,129 +807,32 @@ public class ProjectController extends CayenneController {
         }
     }
 
-    public void rewindForward() {
-        int size = controllerStateHistory.size();
-        if (size < 2) {
-            return;
-        }
-
-        int i = controllerStateHistory.indexOf(state);
-        ControllerState cs;
-
-        int counter = 0;
-        while (true) {
-            if (i < 0) {
-                // A new state was created without it being saved. Move to the beginning of the list
-                cs = controllerStateHistory.get(0);
-            } else if (i + 1 < size) {
-                // move forward
-                cs = controllerStateHistory.get(i + 1);
-            } else {
-                // wrap around
-                cs = controllerStateHistory.get(0);
-            }
-
-            if (cs.isNotSame(state)) {
-                break;
-            }
-
-            // if it doesn't find it within 5 tries it is probably stuck in a loop
-            if (++counter > 5) {
-                break;
-            }
-
-            i++;
-        }
-
-        replayNavigationState(cs);
-    }
 
     public void rewindBackwards() {
-        int size = controllerStateHistory.size();
-        if (size < 2) {
-            return;
-        }
-
-        int i = controllerStateHistory.indexOf(state);
-        ControllerState cs;
-
-        int counter = 0;
-        while (true) {
-            if (i < 0) {
-                // a new state got created without it being saved.
-                try {
-                    cs = controllerStateHistory.get(size - 2);
-                } catch (ArrayIndexOutOfBoundsException ex) {
-                    cs = controllerStateHistory.get(size - 1);
-                }
-            } else if (i - 1 >= 0) {
-                // move to the previous one
-                cs = controllerStateHistory.get(i - 1);
-            } else {
-                // wrap around
-                cs = controllerStateHistory.get(size - 1);
-            }
-
-            if (cs.isNotSame(state)) {
-                break;
-            }
-
-            // if it doesn't find it within 5 tries it is probably stuck in a loop
-            if (++counter > 5) {
-                break;
-            }
-
-            i--;
-
-        }
-
-        replayNavigationState(cs);
+       history.replayLastEvent(this);
     }
 
-    private void replayNavigationState(ControllerState cs) {
-        state = cs;
-        DisplayEvent de = cs.event;
-        if (de == null) {
-            return;
-        }
-
-        if (de instanceof EntityDisplayEvent) {
-            EntityDisplayEvent ede = (EntityDisplayEvent) de;
-            if (ede.getEntity() instanceof ObjEntity) {
-                fireObjEntitySelected(ede);
-            } else if (ede.getEntity() instanceof DbEntity) {
-                fireDbEntitySelected(ede);
-            }
-        } else if (de instanceof EmbeddableDisplayEvent) {
-            fireEmbeddableSelected((EmbeddableDisplayEvent) de);
-        } else if (de instanceof ProcedureDisplayEvent) {
-            fireProcedureSelected((ProcedureDisplayEvent) de);
-        } else if (de instanceof QueryDisplayEvent) {
-            fireQuerySelected((QueryDisplayEvent) de);
-        } else if (de instanceof DataMapDisplayEvent) {
-            fireDataMapSelected((DataMapDisplayEvent) de);
-        } else if (de instanceof DataNodeDisplayEvent) {
-            fireDataNodeSelected((DataNodeDisplayEvent) de);
-        } else if (de instanceof DomainDisplayEvent) {
-            fireDomainSelected((DomainDisplayEvent) de);
-        }
+    public void rewindForward() {
+        history.replayNextEvent(this);
     }
 
     public void fireObjEntitySelected(EntityDisplayEvent e) {
-        LOGGER.debug("fireObjEntitySelected: {}", e.getEntity().getName());
+        boolean changed = e.getEntity() != state.objEntity;
+
+        LOGGER.debug("fireObjEntitySelected: {}, changed: {}", e.getEntity().getName(), changed);
 
-        if (e.getEntity() != state.objEntity) {
-            clearState();
+        if (changed) {
+            state = new ControllerState();
             state.dataDomain = e.getDomain();
             state.dataNode = e.getDataNode();
             state.dataMap = e.getDataMap();
             state.objEntity = (ObjEntity) e.getEntity();
 
-            saveState(e);
-        }
+            history.recordEvent(e);
 
-        for (ObjEntityDisplayListener l : listeners.getListeners(ObjEntityDisplayListener.class)) {
-            l.currentObjEntityChanged(e);
+            for (ObjEntityDisplayListener l : listeners.getListeners(ObjEntityDisplayListener.class)) {
+                l.currentObjEntityChanged(e);
+            }
         }
     }
 
@@ -997,12 +840,12 @@ public class ProjectController extends CayenneController {
         LOGGER.debug("fireEmbeddableSelected: {}", e.getEmbeddable().getClassName());
 
         if (e.getEmbeddable() != state.embeddable) {
-            clearState();
+            state = new ControllerState();
             state.dataDomain = e.getDomain();
             state.dataNode = e.getDataNode();
             state.dataMap = e.getDataMap();
             state.embeddable = e.getEmbeddable();
-            saveState(e);
+            history.recordEvent(e);
         }
 
         for (EmbeddableDisplayListener l : listeners.getListeners(EmbeddableDisplayListener.class)) {
@@ -1014,12 +857,12 @@ public class ProjectController extends CayenneController {
         LOGGER.debug("fireQuerySelected: {}", e.getQuery().getName());
 
         if (e.getQuery() != state.query) {
-            clearState();
+            state = new ControllerState();
             state.dataDomain = e.getDomain();
             state.dataMap = e.getDataMap();
             state.query = e.getQuery();
 
-            saveState(e);
+            history.recordEvent(e);
         }
 
         for (QueryDisplayListener l : listeners.getListeners(QueryDisplayListener.class)) {
@@ -1031,11 +874,11 @@ public class ProjectController extends CayenneController {
         LOGGER.debug("fireProcedureSelected: {}", e.getProcedure().getName());
 
         if (e.getProcedure() != state.procedure) {
-            clearState();
+            state = new ControllerState();
             state.dataDomain = e.getDomain();
             state.dataMap = e.getDataMap();
             state.procedure = e.getProcedure();
-            saveState(e);
+            history.recordEvent(e);
         }
 
         for (ProcedureDisplayListener l : listeners.getListeners(ProcedureDisplayListener.class)) {
@@ -1048,7 +891,7 @@ public class ProjectController extends CayenneController {
 
         if (!Arrays.equals(e.getProcedureParameters(), state.procedureParameters)) {
             if (state.procedure != e.getProcedure()) {
-                clearState();
+                state = new ControllerState();
                 state.dataDomain = e.getDomain();
                 state.dataMap = e.getDataMap();
                 state.procedure = e.getProcedure();
@@ -1065,12 +908,12 @@ public class ProjectController extends CayenneController {
         LOGGER.debug("fireDbEntitySelected: {}", e.getEntity().getName());
 
         if (e.getEntity() != state.dbEntity) {
-            clearState();
+            state = new ControllerState();
             state.dataDomain = e.getDomain();
             state.dataNode = e.getDataNode();
             state.dataMap = e.getDataMap();
             state.dbEntity = (DbEntity) e.getEntity();
-            saveState(e);
+            history.recordEvent(e);
         }
 
         for (DbEntityDisplayListener l : listeners.getListeners(DbEntityDisplayListener.class)) {
@@ -1103,11 +946,11 @@ public class ProjectController extends CayenneController {
     }
 
     public void fireDbAttributeSelected(AttributeDisplayEvent e) {
-        LOGGER.debug("fireDbAttributeSelected: {}", Arrays.stream(e.getAttributes()).map(a -> a.getName()).toArray());
+        LOGGER.debug("fireDbAttributeSelected: {}", Arrays.stream(e.getAttributes()).map(Attribute::getName).toArray());
 
         if (!Arrays.equals(e.getAttributes(), state.dbAttributes)) {
             if (e.getEntity() != state.dbEntity) {
-                clearState();
+                state = new ControllerState();
                 state.dataDomain = e.getDomain();
                 state.dataMap = e.getDataMap();
                 state.dbEntity = (DbEntity) e.getEntity();
@@ -1146,11 +989,11 @@ public class ProjectController extends CayenneController {
     }
 
     public void fireObjAttributeSelected(AttributeDisplayEvent e) {
-        LOGGER.debug("fireObjAttributeSelected: {}", Arrays.stream(e.getAttributes()).map(a -> a.getName()).toArray());
+        LOGGER.debug("fireObjAttributeSelected: {}", Arrays.stream(e.getAttributes()).map(Attribute::getName).toArray());
 
         if (!Arrays.equals(e.getAttributes(), state.objAttributes)) {
             if (e.getEntity() != state.objEntity) {
-                clearState();
+                state = new ControllerState();
                 state.dataDomain = e.getDomain();
                 state.dataMap = e.getDataMap();
                 state.objEntity = (ObjEntity) e.getEntity();
@@ -1165,11 +1008,11 @@ public class ProjectController extends CayenneController {
     }
 
     public void fireEmbeddableAttributeSelected(EmbeddableAttributeDisplayEvent ev) {
-        LOGGER.debug("fireEmbeddableAttributeSelected: {}", Arrays.stream(ev.getEmbeddableAttributes()).map(a -> a.getName()).toArray());
+        LOGGER.debug("fireEmbeddableAttributeSelected: {}", Arrays.stream(ev.getEmbeddableAttributes()).map(EmbeddableAttribute::getName).toArray());
 
         if (!Arrays.equals(ev.getEmbeddableAttributes(), state.embeddableAttributes)) {
             if (ev.getEmbeddable() != state.embeddable) {
-                clearState();
+                state = new ControllerState();
                 state.dataDomain = ev.getDomain();
                 state.dataMap = ev.getDataMap();
                 state.embeddable = ev.getEmbeddable();
@@ -1212,10 +1055,10 @@ public class ProjectController extends CayenneController {
     }
 
     public void fireDbRelationshipSelected(RelationshipDisplayEvent e) {
-        LOGGER.debug("fireDbRelationshipSelected: {}", Arrays.stream(e.getRelationships()).map(r -> r.getName()).toArray());
+        LOGGER.debug("fireDbRelationshipSelected: {}", Arrays.stream(e.getRelationships()).map(Relationship::getName).toArray());
         if (!Arrays.equals(e.getRelationships(), state.dbRelationships)) {
             if (e.getEntity() != state.dbEntity) {
-                clearState();
+                state = new ControllerState();
                 state.dataDomain = e.getDomain();
                 state.dataMap = e.getDataMap();
                 state.dbEntity = (DbEntity) e.getEntity();
@@ -1255,7 +1098,7 @@ public class ProjectController extends CayenneController {
 
     public void fireMultipleObjectsSelected(MultipleObjectsDisplayEvent e) {
         LOGGER.debug("fireMultipleObjectsSelected");
-        clearState();
+        state = new ControllerState();
         state.paths = e.getNodes();
         state.parentPath = e.getParentNode();
 
@@ -1265,10 +1108,10 @@ public class ProjectController extends CayenneController {
     }
 
     public void fireObjRelationshipSelected(RelationshipDisplayEvent e) {
-        LOGGER.debug("fireObjRelationshipSelected: {}", Arrays.stream(e.getRelationships()).map(r -> r.getName()).toArray());
+        LOGGER.debug("fireObjRelationshipSelected: {}", Arrays.stream(e.getRelationships()).map(Relationship::getName).toArray());
         if (!Arrays.equals(e.getRelationships(), state.objRelationships)) {
             if (e.getEntity() != state.objEntity) {
-                clearState();
+                state = new ControllerState();
                 state.dataDomain = e.getDomain();
                 state.dataMap = e.getDataMap();
                 state.objEntity = (ObjEntity) e.getEntity();
@@ -1488,9 +1331,10 @@ public class ProjectController extends CayenneController {
         }
     }
 
+
+
     static class ControllerState {
 
-        private DisplayEvent event;
         private DataChannelDescriptor dataDomain;
         private DataNodeDescriptor dataNode;
         private DataMap dataMap;
@@ -1525,38 +1369,5 @@ public class ProjectController extends CayenneController {
 
             callbackMethods = new ObjCallbackMethod[0];
         }
-
-        /*
-         * Used to determine if the val ControllerState is equivalent, which means if the event is refired again, will
-         * it end up in the same place on the screen. This gets a bit messy at the end, because of inheritance heirarchy
-         * issues.
-         */
-        public boolean isNotSame(ControllerState val) {
-
-            if (val == null)
-                return true;
-
-            if (event instanceof EntityDisplayEvent && val.event instanceof EntityDisplayEvent) {
-                if (((EntityDisplayEvent) val.event).getEntity() instanceof ObjEntity) {
-                    return objEntity != val.objEntity;
-                } else {
-                    return dbEntity != val.dbEntity;
-                }
-            } else if (event instanceof ProcedureDisplayEvent && val.event instanceof ProcedureDisplayEvent) {
-                return procedure != val.procedure;
-            } else if (event instanceof QueryDisplayEvent && val.event instanceof QueryDisplayEvent) {
-                return query != val.query;
-            } else if (event instanceof EmbeddableDisplayEvent && val.event instanceof EmbeddableDisplayEvent) {
-                return embeddable != val.embeddable;
-            } else if (event.getClass() == DataMapDisplayEvent.class && event.getClass() == val.event.getClass()) {
-                return dataMap != val.dataMap;
-            } else if (event.getClass() == DataNodeDisplayEvent.class && event.getClass() == val.event.getClass()) {
-                return dataNode != val.dataNode;
-            } else if (event.getClass() == DomainDisplayEvent.class && event.getClass() == val.event.getClass()) {
-                return dataDomain != val.dataDomain;
-            }
-
-            return true;
-        }
     }
 }
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/util/CircularArray.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/util/CircularArray.java
deleted file mode 100644
index da93b7a30..000000000
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/util/CircularArray.java
+++ /dev/null
@@ -1,398 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-
-package org.apache.cayenne.modeler.util;
-
-/**
- * A circular array is an array of fixed size as objects are added it will push
- * objects off of the end to allow space for new objects to be added. This is
- * useful for things like a fixed history size for a navigation tool.
- * 
- * @since 1.2
- */
-public class CircularArray<T>  {
-
-    private Object[] array = null;
-    private int head = 0;
-    private int tail = 0;
-    private int count = 0;
-    private int capacity = 0;
-
-    /**
-     * Creates an array of capacity size.
-     * 
-     * @param capacity
-     *            - size of the new array
-     */
-    public CircularArray(int capacity) {
-        if (capacity <= 0) {
-            throw new IllegalArgumentException("Capacity must be greater than zero");
-        }
-        array = new Object[capacity];
-        this.capacity = capacity;
-    }
-
-    /**
-     * Clears out the contents of the array.
-     */
-    public void clear() {
-        array = new Object[capacity];
-        head = 0;
-        tail = 0;
-        count = 0;
-    }
-
-    /**
-     * Returns true if the array has no elements;
-     */
-    public boolean isEmpty() {
-        return count == 0;
-    }
-
-    /**
-     * Adds a new object to the array. If the array is full it will push the
-     * oldest item out of the array.
-     * 
-     * @param obj
-     *            - the object to be added
-     */
-    public void add(Object obj) {
-        // we have wrapped and we have to move the head pointer
-        if (count == capacity && tail == head) {
-            head = (head + 1) % capacity;
-        }
-
-        array[tail] = obj;
-
-        tail = (tail + 1) % capacity;
-
-        count++;
-        if (count > capacity)
-            count = capacity;
-    }
-
-    /**
-     * Returns the number of objects stored in the array.
-     */
-    public int capacity() {
-        return capacity;
-    }
-
-    /*
-     * Converts the logical index into a physical index.
-     */
-    int convert(int index) {
-        return (index + head) % capacity;
-    }
-
-    /*
-     * Makes sure that the index is within range.
-     */
-    private void rangeCheck(int index) {
-        if (index >= capacity || index < 0) {
-            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + capacity);
-        }
-    }
-
-    /**
-     * Returns true if the array contains the specified object.
-     * 
-     * @param obj
-     *            the object to be checked
-     */
-    public boolean contains(T obj) {
-        return indexOf(obj) >= 0;
-    }
-
-    /**
-     * Gets the object at the specified index.
-     * 
-     * @param index
-     *            the index of the object to be retrieved
-     */
-    @SuppressWarnings("unchecked")
-    public T get(int index) {
-        rangeCheck(index);
-        if (count == 0) {
-            return null;
-        }
-        return (T)array[convert(index)];
-    }
-
-    /**
-     * Returns the index of the specified object
-     * 
-     * @param obj
-     *            the object that is being searched for
-     */
-    public int indexOf(T obj) {
-        for (int i = 0; i < capacity; i++) {
-            int index = convert(i);
-            if (array[index] == obj) {
-                return i;
-            }
-        }
-        // not found
-        return -1;
-    }
-
-    /**
-     * Removes the specified object from the array
-     *
-     * @param obj object to be removed
-     */
-    public void remove(T obj) {
-        if (count == 0) {
-            return;
-        }
-
-        int i = indexOf(obj);
-
-        while (i >= 0) {
-            // convert from logical to physical location
-            int pos = convert(i);
-
-            if (pos == head) {
-                // move the head up one
-                head = (head + 1) % capacity;
-                array[pos] = null;
-                count--;
-            } else if (pos == tail) {
-                // move the tail back one
-                tail = (tail - 1 + capacity) % capacity;
-                array[pos] = null;
-                count--;
-            } else {
-                // create a brand new array and start it back out at zero
-                Object[] a = new Object[capacity];
-                int destPos = 0;
-                int len = 0;
-
-                if (head == tail) {
-                    // most likeley scenario when it is full
-                    if (head < pos) {
-                        // copy from head to position
-                        len = pos - head;
-                        System.arraycopy(array, head, a, destPos, len);
-                        destPos += len;
-
-                        // copy from pos +1 to end
-                        len = (capacity - 1) - pos;
-                        if (len > 0) {
-                            System.arraycopy(array, pos + 1, a, destPos, len);
-                            destPos += len;
-                        }
-
-                        // copy from zero to head
-                        len = head;
-                        if (len > 0) {
-                            System.arraycopy(array, 0, a, destPos, len);
-                        }
-                    } else if (head > pos) {
-                        // copy from head to end of array
-                        len = capacity - head;
-                        if (len > 0) {
-                            System.arraycopy(array, head, a, destPos, len);
-                            destPos += len;
-                        }
-
-                        // copy from zero to pos -1
-                        len = pos;
-                        if (len > 0) {
-                            System.arraycopy(array, 0, a, destPos, len);
-                            destPos += len;
-                        }
-
-                        // copy from pos + 1 to tail
-                        len = tail - pos - 1;
-                        if (len > 0) {
-                            System.arraycopy(array, pos + 1, a, destPos, len);
-                        }
-                    }
-                } else if (head < tail) {
-                    // copy from head to position -1
-                    len = pos - head;
-                    if (len > 0) {
-                        System.arraycopy(array, head, a, destPos, len);
-                        destPos += len;
-                    }
-
-                    // copy from position + 1 to tail
-                    len = tail - pos;
-                    if (len > 0) {
-                        System.arraycopy(array, pos + 1, a, destPos, len);
-                        destPos += len;
-                    }
-                } else if (head > tail) {
-                    if (head < pos) {
-                        // copy from head to position
-                        len = pos - head;
-                        System.arraycopy(array, head, a, destPos, len);
-                        destPos += len;
-
-                        // copy from pos +1 to end
-                        len = capacity - 1 - pos;
-                        if (len > 0) {
-                            System.arraycopy(array, pos + 1, a, destPos, len);
-                            destPos += len;
-                        }
-
-                        // copy from beginning to tail
-                        len = tail;
-                        if (len > 0) {
-                            System.arraycopy(array, 0, a, destPos, len);
-                        }
-                    } else if (head > pos) {
-                        // copy from head to end of array
-                        len = capacity - head;
-                        if (len > 0) {
-                            System.arraycopy(array, head, a, destPos, len);
-                            destPos += len;
-                        }
-
-                        // copy from zero to pos -1
-                        len = pos - 1;
-                        if (len > 0) {
-                            System.arraycopy(array, 0, a, destPos, len);
-                            destPos += len;
-                        }
-
-                        // copy from pos+1 to tail
-                        len = tail - pos;
-                        if (len > 0) {
-                            System.arraycopy(array, pos + 1, a, destPos, len);
-                        }
-                    }
-                }
-                count--;
-                array = a;
-                head = 0;
-                tail = count;
-            }
-            i = indexOf(obj);
-        }
-    }
-
-    /**
-     * Resizes the array to the specified new size. If the new capacity is
-     * smaller than the current object count in the array, it will keep the
-     * newCapacity most recent objects.
-     * 
-     * @param newCapacity
-     *            the new capacity of the array
-     */
-    public void resize(int newCapacity) {
-        int i = 0;
-        int offset = 0;
-        if (newCapacity < count) {
-            // making it smaller so we want to readjust the first object
-            // to be copied into the new array
-            i = count - newCapacity;
-            offset = count - newCapacity;
-        }
-        Object newArray[] = new Object[newCapacity];
-        for (; i < count; i++) {
-            newArray[i - offset] = array[convert(i)];
-        }
-        head = 0;
-        tail = 0;
-        capacity = newCapacity;
-
-        // adjust the count if it is more than the new capacity
-        if (capacity < count)
-            count = capacity;
-        array = newArray;
-    }
-
-    /**
-     * Returns the number of objects stored in the array.
-     */
-    public int size() {
-        return count;
-    }
-
-    /**
-     * Converts the array to an Object array.
-     */
-    public T[] toArray() {
-        Object[] o = new Object[capacity];
-        for (int i = 0; i < capacity; i++) {
-            o[i] = array[convert(i)];
-        }
-        return (T[])o;
-    }
-
-    public String internalRep() {
-        Object o;
-        StringBuilder sb = new StringBuilder();
-        sb.append("\n");
-        for (int i = 0; i < array.length; i++) {
-            sb.append('(').append(i).append(")  ");
-
-            o = array[i];
-            if (o == null) {
-                sb.append("null");
-            } else {
-                sb.append(o.toString());
-            }
-            if (i == head || i == tail) {
-                sb.append('<');
-                if (i == head)
-                    sb.append("h");
-                if (i == tail)
-                    sb.append("t");
-            }
-            sb.append("\n");
-        }
-
-        sb.append("count = [");
-        sb.append(count);
-        sb.append("]");
-
-        sb.append("\nhead  = [");
-        sb.append(head);
-        sb.append("]");
-
-        sb.append("\ntail  = [");
-        sb.append(tail);
-        sb.append("]");
-
-        return sb.toString();
-    }
-
-    public String toString() {
-        Object[] oa = toArray();
-        Object o;
-        StringBuilder sb = new StringBuilder();
-        sb.append("[");
-        for (int i = 0; i < oa.length; i++) {
-            o = oa[i];
-            if (i > 0) {
-                sb.append(", ");
-            }
-            if (o == null) {
-                sb.append("null");
-            } else {
-                sb.append(o.toString());
-            }
-        }
-        sb.append("]");
-        return sb.toString();
-    }
-}
diff --git a/modeler/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/util/CircularArrayTest.java b/modeler/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/util/CircularArrayTest.java
deleted file mode 100644
index 0426e7efa..000000000
--- a/modeler/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/util/CircularArrayTest.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-
-package org.apache.cayenne.modeler.util;
-
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-
-public class CircularArrayTest {
-
-    @Test
-    public void testArraySize5() {
-
-        String a = "A", b = "B", c = "C", d = "D", e = "E", f = "F", g = "G", h = "H";
-        CircularArray<String> q = new CircularArray<>(5);
-
-        assertAdd(q, a, "[A, null, null, null, null]");
-        assertRemove(q, a, "[null, null, null, null, null]");
-        assertAdd(q, a, "[A, null, null, null, null]");
-
-        assertAdd(q, b, "[A, B, null, null, null]");
-        assertRemove(q, b, "[A, null, null, null, null]");
-        assertAdd(q, b, "[A, B, null, null, null]");
-
-        assertAdd(q, c, "[A, B, C, null, null]");
-        assertRemove(q, c, "[A, B, null, null, null]");
-        assertAdd(q, c, "[A, B, C, null, null]");
-
-        assertAdd(q, d, "[A, B, C, D, null]");
-        assertRemove(q, d, "[A, B, C, null, null]");
-        assertAdd(q, d, "[A, B, C, D, null]");
-
-        assertAdd(q, e, "[A, B, C, D, E]");
-        assertRemove(q, e, "[A, B, C, D, null]");
-        assertAdd(q, e, "[A, B, C, D, E]");
-
-        assertAdd(q, f, "[B, C, D, E, F]");
-        assertRemove(q, f, "[B, C, D, E, null]");
-        assertAdd(q, f, "[B, C, D, E, F]");
-
-        assertAdd(q, g, "[C, D, E, F, G]");
-        assertRemove(q, e, "[C, D, F, G, null]");
-        assertAdd(q, h, "[C, D, F, G, H]");
-
-        assertRemove(q, c, "[D, F, G, H, null]");
-        assertRemove(q, h, "[D, F, G, null, null]");
-        assertRemove(q, f, "[D, G, null, null, null]");
-        assertRemove(q, g, "[D, null, null, null, null]");
-        assertRemove(q, d, "[null, null, null, null, null]");
-    }
-
-    @Test
-    public void testArraySize3() {
-        String a = "A", b = "B", c = "C", d = "D", e = "E";
-        CircularArray<String> q = new CircularArray<>(3);
-
-        assertEquals(0, q.size());
-        assertEquals(3, q.capacity());
-
-        q.add(a);
-
-        assertEquals(0, q.indexOf(a));
-        assertEquals(a, q.get(0));
-        assertEquals(1, q.size());
-
-        q.add(b);
-
-        assertEquals(1, q.indexOf(b));
-        assertEquals(a, q.get(0));
-        assertEquals(b, q.get(1));
-        assertEquals(2, q.size());
-
-        q.add(c);
-
-        assertEquals(2, q.indexOf(c));
-        assertEquals(a, q.get(0));
-        assertEquals(b, q.get(1));
-        assertEquals(c, q.get(2));
-        assertEquals(3, q.size());
-
-        q.add(d);
-
-        assertEquals(3, q.size());
-
-        q.add(e);
-
-        assertEquals(3, q.size());
-        assertFalse("A should not be in the q", q.contains(a));
-        assertEquals(0, q.indexOf(c));
-        assertEquals(c, q.get(0));
-        assertEquals(1, q.indexOf(d));
-        assertEquals(d, q.get(1));
-        assertEquals(2, q.indexOf(e));
-        assertEquals(e, q.get(2));
-
-        // should be the same after resizing
-        q.resize(5);
-
-        assertEquals(5, q.capacity());
-        assertEquals(3, q.size());
-        assertEquals(2, q.indexOf(e));
-        assertEquals(e, q.get(2));
-
-        q.resize(2);
-
-        assertEquals(2, q.capacity());
-    }
-
-    @Test
-    public void testToString() {
-        CircularArray<String> a = new CircularArray<>(5);
-        assertEquals("[null, null, null, null, null]", a.toString());
-    }
-
-    public void assertAdd(CircularArray<String> a, String obj, String expected) {
-        a.add(obj);
-        assertEquals(expected, a.toString());
-    }
-
-    public void assertRemove(CircularArray<String> a, String obj, String expected) {
-        a.indexOf(obj);
-        a.remove(obj);
-        assertEquals(expected, a.toString());
-    }
-}