You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jmeter.apache.org by pm...@apache.org on 2017/09/28 19:12:45 UTC

svn commit: r1810034 - in /jmeter/trunk/src/core/org/apache/jmeter/gui: ./ action/ tree/

Author: pmouawad
Date: Thu Sep 28 19:12:44 2017
New Revision: 1810034

URL: http://svn.apache.org/viewvc?rev=1810034&view=rev
Log:
Bug 57039 - Inconsistency with the undo/redo log
This closes #310
Contributed by Emilian Bold
Bugzilla Id: 57039

Added:
    jmeter/trunk/src/core/org/apache/jmeter/gui/GlobalUndoableEdit.java   (with props)
    jmeter/trunk/src/core/org/apache/jmeter/gui/SimpleCompoundEdit.java   (with props)
    jmeter/trunk/src/core/org/apache/jmeter/gui/TreeState.java   (with props)
Modified:
    jmeter/trunk/src/core/org/apache/jmeter/gui/GuiPackage.java
    jmeter/trunk/src/core/org/apache/jmeter/gui/UndoHistory.java
    jmeter/trunk/src/core/org/apache/jmeter/gui/UndoHistoryItem.java
    jmeter/trunk/src/core/org/apache/jmeter/gui/action/ActionRouter.java
    jmeter/trunk/src/core/org/apache/jmeter/gui/action/CheckDirty.java
    jmeter/trunk/src/core/org/apache/jmeter/gui/action/ResetSearchCommand.java
    jmeter/trunk/src/core/org/apache/jmeter/gui/action/SearchTreeDialog.java
    jmeter/trunk/src/core/org/apache/jmeter/gui/action/UndoCommand.java
    jmeter/trunk/src/core/org/apache/jmeter/gui/tree/JMeterTreeNode.java

Added: jmeter/trunk/src/core/org/apache/jmeter/gui/GlobalUndoableEdit.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/gui/GlobalUndoableEdit.java?rev=1810034&view=auto
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/gui/GlobalUndoableEdit.java (added)
+++ jmeter/trunk/src/core/org/apache/jmeter/gui/GlobalUndoableEdit.java Thu Sep 28 19:12:44 2017
@@ -0,0 +1,55 @@
+/*
+ * 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
+ *
+ *   http://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.jmeter.gui;
+
+import java.util.function.Consumer;
+import javax.swing.undo.AbstractUndoableEdit;
+import javax.swing.undo.CannotRedoException;
+import javax.swing.undo.CannotUndoException;
+
+public final class GlobalUndoableEdit extends AbstractUndoableEdit {
+
+    /**
+     * 
+     */
+    private static final long serialVersionUID = -4964577622742131354L;
+    private final UndoHistoryItem item;
+    private final UndoHistoryItem previous;
+    private final Consumer<UndoHistoryItem> loader;
+
+    public GlobalUndoableEdit(UndoHistoryItem item, UndoHistoryItem previous, Consumer<UndoHistoryItem> loader) {
+        this.item = item;
+        this.previous = previous;
+        this.loader = loader;
+    }
+
+    @Override
+    public void undo() throws CannotUndoException {
+        super.undo();
+
+        loader.accept(previous);
+    }
+
+    @Override
+    public void redo() throws CannotRedoException {
+        super.redo();
+
+        loader.accept(item);
+    }
+}

Propchange: jmeter/trunk/src/core/org/apache/jmeter/gui/GlobalUndoableEdit.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: jmeter/trunk/src/core/org/apache/jmeter/gui/GlobalUndoableEdit.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: jmeter/trunk/src/core/org/apache/jmeter/gui/GuiPackage.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/gui/GuiPackage.java?rev=1810034&r1=1810033&r2=1810034&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/gui/GuiPackage.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/gui/GuiPackage.java Thu Sep 28 19:12:44 2017
@@ -810,12 +810,17 @@ public final class GuiPackage implements
     }
 
     /**
-     * Navigate back and forward through undo history
-     *
-     * @param offset int
+     * Navigate back through undo history
      */
-    public void goInHistory(int offset) {
-        undoHistory.moveInHistory(offset, this.treeModel);
+    public void undo() {
+        undoHistory.undo();
+    }
+
+    /**
+     * Navigate forward through undo history
+     */
+    public void redo() {
+        undoHistory.redo();
     }
 
     /**
@@ -901,4 +906,18 @@ public final class GuiPackage implements
     public GuiLogEventBus getLogEventBus() {
         return logEventBus;
     }
+
+    /**
+     * Begin a group of actions modeled as 1 undo
+     */
+    public void beginUndoTransaction() {
+        undoHistory.beginUndoTransaction();
+    }
+
+    /**
+     * End a group of actions modeled as 1 undo
+     */
+    public void endUndoTransaction() {
+        undoHistory.endUndoTransaction();
+    }
 }

Added: jmeter/trunk/src/core/org/apache/jmeter/gui/SimpleCompoundEdit.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/gui/SimpleCompoundEdit.java?rev=1810034&view=auto
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/gui/SimpleCompoundEdit.java (added)
+++ jmeter/trunk/src/core/org/apache/jmeter/gui/SimpleCompoundEdit.java Thu Sep 28 19:12:44 2017
@@ -0,0 +1,43 @@
+/*
+ * 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
+ *
+ *   http://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.jmeter.gui;
+
+import javax.swing.undo.CompoundEdit;
+
+public class SimpleCompoundEdit extends CompoundEdit {
+
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 7125085226441904495L;
+
+    /**
+     * @return boolean true if edits is empty
+     */
+    public boolean isEmpty() {
+        return edits.isEmpty();
+    }
+
+    /**
+     * @return size of edits
+     */
+    public int size() {
+        return edits.size();
+    }
+}

Propchange: jmeter/trunk/src/core/org/apache/jmeter/gui/SimpleCompoundEdit.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: jmeter/trunk/src/core/org/apache/jmeter/gui/SimpleCompoundEdit.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: jmeter/trunk/src/core/org/apache/jmeter/gui/TreeState.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/gui/TreeState.java?rev=1810034&view=auto
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/gui/TreeState.java (added)
+++ jmeter/trunk/src/core/org/apache/jmeter/gui/TreeState.java Thu Sep 28 19:12:44 2017
@@ -0,0 +1,98 @@
+/*
+ * 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
+ *
+ *   http://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.jmeter.gui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JTree;
+
+public interface TreeState {
+
+    /**
+     * Restore tree expanded and selected state
+     *
+     * @param guiInstance GuiPackage to be used
+     */
+    void restore(GuiPackage guiInstance);
+
+    static final TreeState NOTHING = (GuiPackage guiInstance) -> {};
+
+    /**
+     * Save tree expanded and selected state
+     *
+     * @param guiPackage {@link GuiPackage} to be used
+     */
+    public static TreeState from(GuiPackage guiPackage) {
+        if (guiPackage == null) {
+            return NOTHING;
+        }
+
+        MainFrame mainframe = guiPackage.getMainFrame();
+        if (mainframe != null) {
+            final JTree tree = mainframe.getTree();
+            int savedSelected = tree.getMinSelectionRow();
+            ArrayList<Integer> savedExpanded = new ArrayList<>();
+
+            for (int rowN = 0; rowN < tree.getRowCount(); rowN++) {
+                if (tree.isExpanded(rowN)) {
+                    savedExpanded.add(rowN);
+                }
+            }
+
+            return new TreeStateImpl(savedSelected, savedExpanded);
+        }
+
+        return NOTHING;
+    }
+
+    static final class TreeStateImpl implements TreeState {
+
+        // GUI tree expansion state
+        private final List<Integer> savedExpanded;
+
+        // GUI tree selected row
+        private final int savedSelected;
+
+        public TreeStateImpl(int savedSelected, List<Integer> savedExpanded) {
+            this.savedSelected = savedSelected;
+            this.savedExpanded = savedExpanded;
+        }
+
+        @Override
+        public void restore(GuiPackage guiInstance) {
+            MainFrame mainframe = guiInstance.getMainFrame();
+            if (mainframe == null) {
+                //log?
+                return;
+            }
+
+            final JTree tree = mainframe.getTree();
+
+            if (!savedExpanded.isEmpty()) {
+                for (int rowN : savedExpanded) {
+                    tree.expandRow(rowN);
+                }
+            } else {
+                tree.expandRow(0);
+            }
+            tree.setSelectionRow(savedSelected);
+        }
+    }
+}

Propchange: jmeter/trunk/src/core/org/apache/jmeter/gui/TreeState.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: jmeter/trunk/src/core/org/apache/jmeter/gui/TreeState.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: jmeter/trunk/src/core/org/apache/jmeter/gui/UndoHistory.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/gui/UndoHistory.java?rev=1810034&r1=1810033&r2=1810034&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/gui/UndoHistory.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/gui/UndoHistory.java Thu Sep 28 19:12:44 2017
@@ -21,10 +21,12 @@ package org.apache.jmeter.gui;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Stack;
 
-import javax.swing.JTree;
 import javax.swing.event.TreeModelEvent;
 import javax.swing.event.TreeModelListener;
+import javax.swing.undo.UndoManager;
+import javax.swing.undo.UndoableEdit;
 
 import org.apache.jmeter.gui.action.UndoCommand;
 import org.apache.jmeter.gui.tree.JMeterTreeModel;
@@ -53,50 +55,10 @@ public class UndoHistory implements Tree
         void notifyChangeInHistory(UndoHistory history);
     }
 
-    /**
-     * Avoid storing too many elements
-     *
-     * @param <T> Class that should be held in this container
-     */
-    private static class LimitedArrayList<T> extends ArrayList<T> {
-        /**
-         *
-         */
-        private static final long serialVersionUID = -6574380490156356507L;
-        private int limit;
-
-        public LimitedArrayList(int limit) {
-            this.limit = limit;
-        }
-
-        @Override
-        public boolean add(T item) {
-            if (this.size() + 1 > limit) {
-                this.remove(0);
-            }
-            return super.add(item);
-        }
-    }
-
     private static final Logger log = LoggerFactory.getLogger(UndoHistory.class);
 
-    /**
-     * temporary storage for GUI tree expansion state
-     */
-    private ArrayList<Integer> savedExpanded = new ArrayList<>();
-
-    /**
-     * temporary storage for GUI tree selected row
-     */
-    private int savedSelected = 0;
-
-    private static final int INITIAL_POS = -1;
-    private int position = INITIAL_POS;
-
     private static final int HISTORY_SIZE = JMeterUtils.getPropDefault("undo.history.size", 0);
 
-    private List<UndoHistoryItem> history = new LimitedArrayList<>(HISTORY_SIZE);
-
     /**
      * flag to prevent recursive actions
      */
@@ -107,7 +69,14 @@ public class UndoHistory implements Tree
      */
     private List<HistoryListener> listeners = new ArrayList<>();
 
+    private final UndoManager manager = new UndoManager();
+
+    private final Stack<SimpleCompoundEdit> transactions = new Stack<>();
+
+    private UndoHistoryItem lastKnownState = null;
+
     public UndoHistory() {
+        manager.setLimit(HISTORY_SIZE);
     }
 
     /**
@@ -118,8 +87,14 @@ public class UndoHistory implements Tree
             return;
         }
         log.debug("Clearing undo history");
-        history.clear();
-        position = INITIAL_POS;
+        manager.discardAllEdits();
+        if (isTransaction()) {
+            if(log.isWarnEnabled()) {
+                log.warn("Clearing undo history with {} unfinished transactions", transactions.size());
+            }
+            transactions.clear();
+        }
+        lastKnownState = null;
         notifyListeners();
     }
 
@@ -160,63 +135,46 @@ public class UndoHistory implements Tree
         // first clone to not convert original tree
         tree = (HashTree) tree.getTree(tree.getArray()[0]).clone();
 
-        position++;
-        while (history.size() > position) {
-            if (log.isDebugEnabled()) {
-                log.debug("Removing further record, position: {}, size: {}", position, history.size());
-            }
-            history.remove(history.size() - 1);
-        }
-
         // cloning is required because we need to immute stored data
         HashTree copy = UndoCommand.convertAndCloneSubTree(tree);
 
-        history.add(new UndoHistoryItem(copy, comment));
+        GuiPackage guiPackage = GuiPackage.getInstance();
+        //or maybe a Boolean?
+        boolean dirty = guiPackage != null ? guiPackage.isDirty() : false;
+        addEdit(new UndoHistoryItem(copy, comment, TreeState.from(guiPackage), dirty));
 
-        log.debug("Added history element, position: {}, size: {}", position, history.size());
         working = false;
-        notifyListeners();
     }
 
-    /**
-     * Goes through undo history, changing GUI
-     *
-     * @param offset        the direction to go to, usually -1 for undo or 1 for redo
-     * @param acceptorModel TreeModel to accept the changes
-     */
-    public void moveInHistory(int offset, JMeterTreeModel acceptorModel) {
-        log.debug("Moving history from position {} with step {}, size is {}", position, offset, history.size());
-        if (offset < 0 && !canUndo()) {
+    public void undo() {
+        if (!canUndo()) {
             log.warn("Can't undo, we're already on the last record");
             return;
         }
+        manager.undo();
+    }
 
-        if (offset > 0 && !canRedo()) {
+    public void redo() {
+        if (!canRedo()) {
             log.warn("Can't redo, we're already on the first record");
             return;
         }
+        manager.redo();
+    }
 
-        if (history.isEmpty()) {
-            log.warn("Can't proceed, the history is empty");
-            return;
-        }
-
-        position += offset;
-
+    private void reload(UndoHistoryItem z) {
         final GuiPackage guiInstance = GuiPackage.getInstance();
+        JMeterTreeModel acceptorModel = guiInstance.getTreeModel();
 
-        // save tree expansion and selection state before changing the tree
-        saveTreeState(guiInstance);
-
-        // load the tree
-        loadHistoricalTree(acceptorModel, guiInstance);
-
-        // load tree UI state
-        restoreTreeState(guiInstance);
-
-        if (log.isDebugEnabled()) {
-            log.debug("Current position {}, size is {}", position, history.size());
+        try {
+            // load the tree
+            loadHistoricalTree(acceptorModel, guiInstance, z.getTree());
+        } finally {
+            // load tree UI state
+            z.getTreeState().restore(guiInstance);
+            guiInstance.setDirty(z.isDirty());
         }
+        setLastKnownState(z);
 
         // refresh the all ui
         guiInstance.updateCurrentGui();
@@ -230,8 +188,7 @@ public class UndoHistory implements Tree
      * @param acceptorModel tree to accept the data
      * @param guiInstance {@link GuiPackage} to be used
      */
-    private void loadHistoricalTree(JMeterTreeModel acceptorModel, GuiPackage guiInstance) {
-        HashTree newModel = history.get(position).getTree();
+    private void loadHistoricalTree(JMeterTreeModel acceptorModel, GuiPackage guiInstance, HashTree newModel) {
         acceptorModel.removeTreeModelListener(this);
         working = true;
         try {
@@ -239,23 +196,24 @@ public class UndoHistory implements Tree
             guiInstance.addSubTree(newModel);
         } catch (Exception ex) {
             log.error("Failed to load from history", ex);
+        } finally {
+            acceptorModel.addTreeModelListener(this);
+            working = false;
         }
-        acceptorModel.addTreeModelListener(this);
-        working = false;
     }
 
     /**
      * @return true if remaining items
      */
     public boolean canRedo() {
-        return position < history.size() - 1;
+        return manager.canRedo();
     }
 
     /**
      * @return true if not at first element
      */
     public boolean canUndo() {
-        return position > INITIAL_POS + 1;
+        return manager.canUndo();
     }
 
     /**
@@ -308,45 +266,6 @@ public class UndoHistory implements Tree
     }
 
     /**
-     * Save tree expanded and selected state
-     *
-     * @param guiPackage {@link GuiPackage} to be used
-     */
-    private void saveTreeState(GuiPackage guiPackage) {
-        savedExpanded.clear();
-
-        MainFrame mainframe = guiPackage.getMainFrame();
-        if (mainframe != null) {
-            final JTree tree = mainframe.getTree();
-            savedSelected = tree.getMinSelectionRow();
-
-            for (int rowN = 0; rowN < tree.getRowCount(); rowN++) {
-                if (tree.isExpanded(rowN)) {
-                    savedExpanded.add(Integer.valueOf(rowN));
-                }
-            }
-        }
-    }
-
-    /**
-     * Restore tree expanded and selected state
-     *
-     * @param guiInstance GuiPackage to be used
-     */
-    private void restoreTreeState(GuiPackage guiInstance) {
-        final JTree tree = guiInstance.getMainFrame().getTree();
-
-        if (savedExpanded.size() > 0) {
-            for (int rowN : savedExpanded) {
-                tree.expandRow(rowN);
-            }
-        } else {
-            tree.expandRow(0);
-        }
-        tree.setSelectionRow(savedSelected);
-    }
-    
-    /**
      * @return true if history is enabled
      */
     public static boolean isEnabled() {
@@ -370,4 +289,48 @@ public class UndoHistory implements Tree
         }
     }
 
+    private void addEdit(UndoHistoryItem item) {
+        if (lastKnownState != null) {
+            GlobalUndoableEdit edit = new GlobalUndoableEdit(item, lastKnownState, this::reload);
+            addEdit(edit);
+        } else {
+            log.debug("Skipping undo since there is no previous known state");
+        }
+        lastKnownState = item;
+    }
+
+    private void addEdit(UndoableEdit edit) {
+        if (isTransaction()) {
+            transactions.peek().addEdit(edit);
+            //XXX: Add sanity checks for transactions depth and number of edits?
+        } else {
+            manager.addEdit(edit);
+            notifyListeners();
+        }
+    }
+
+    void endUndoTransaction() {
+        if (!isTransaction()) {
+            log.error("Undo transaction ended without beginning", new Exception());
+            return;
+        }
+        SimpleCompoundEdit edit = transactions.pop();
+        edit.end();
+        if (!edit.isEmpty()) {
+            addEdit(edit);
+        }
+    }
+
+    void beginUndoTransaction() {
+        transactions.add(new SimpleCompoundEdit());
+    }
+
+    boolean isTransaction() {
+        return !transactions.isEmpty();
+    }
+
+    private void setLastKnownState(UndoHistoryItem previous) {
+        this.lastKnownState = previous;
+    }
+
 }

Modified: jmeter/trunk/src/core/org/apache/jmeter/gui/UndoHistoryItem.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/gui/UndoHistoryItem.java?rev=1810034&r1=1810033&r2=1810034&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/gui/UndoHistoryItem.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/gui/UndoHistoryItem.java Thu Sep 28 19:12:44 2017
@@ -21,6 +21,7 @@ package org.apache.jmeter.gui;
 import org.apache.jorphan.collections.HashTree;
 
 import java.io.Serializable;
+import org.apache.jmeter.engine.TreeCloner;
 
 /**
  * Undo history item
@@ -35,6 +36,8 @@ public class UndoHistoryItem implements
     private final HashTree tree;
     // TODO: find a way to show this comment in menu item and toolbar tooltip
     private final String comment;
+    private final TreeState treeState;
+    private final boolean dirty;
 
     /**
      * This constructor is for Unit test purposes only
@@ -42,24 +45,39 @@ public class UndoHistoryItem implements
      */
     @Deprecated
     public UndoHistoryItem() {
-        tree = null;
-        comment = null;
+        this(null, null, null, false);
     }
 
     /**
      * @param copy HashTree
      * @param acomment String
      */
-    public UndoHistoryItem(HashTree copy, String acomment) {
+    public UndoHistoryItem(HashTree copy, String acomment, TreeState treeState, boolean dirty) {
         tree = copy;
         comment = acomment;
+        this.treeState = treeState;
+        this.dirty = dirty;
+    }
+
+    public boolean isDirty() {
+        return dirty;
+    }
+
+    public TreeState getTreeState() {
+        return treeState;
     }
 
     /**
      * @return {@link org.apache.jorphan.collections.HashTree}
      */
     public HashTree getTree() {
-        return tree;
+        //EMI: It's important we return a clone here and not the actual tree because
+        // the history item might still be part of some undo action and we don't
+        // want to expose (and corrupt then via edits) internal data
+        TreeCloner cloner = new TreeCloner(false);
+        tree.traverse(cloner);
+
+        return cloner.getClonedTree();
     }
 
     /**

Modified: jmeter/trunk/src/core/org/apache/jmeter/gui/action/ActionRouter.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/gui/action/ActionRouter.java?rev=1810034&r1=1810033&r2=1810034&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/gui/action/ActionRouter.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/gui/action/ActionRouter.java Thu Sep 28 19:12:44 2017
@@ -26,6 +26,7 @@ import java.io.IOException;
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.security.CodeSource;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -57,6 +58,10 @@ public final class ActionRouter implemen
     private final Map<String, Set<ActionListener>> postActionListeners =
             new HashMap<>();
 
+    // New action will clear undo, no point in having a transaction. Same with open
+    // EMI: XXX: Commands could also have an annotation to sigal the Undo preference
+    private final List<String> NO_TRANSACTION_ACTIONS = Arrays.asList(ActionNames.CLOSE, ActionNames.OPEN, ActionNames.OPEN_RECENT);
+
     private ActionRouter() {
     }
 
@@ -67,6 +72,9 @@ public final class ActionRouter implemen
 
     private void performAction(final ActionEvent e) {
         String actionCommand = e.getActionCommand();
+        if(!NO_TRANSACTION_ACTIONS.contains(actionCommand)) {
+            GuiPackage.getInstance().beginUndoTransaction();
+        }
         try {
             try {
                 GuiPackage.getInstance().updateCurrentGui();
@@ -100,6 +108,10 @@ public final class ActionRouter implemen
         } catch (NullPointerException er) {
             log.error("performAction({}) {} caused", actionCommand, e, er);
             JMeterUtils.reportErrorToUser("Sorry, this feature (" + actionCommand + ") not yet implemented");
+        } finally {
+            if(!NO_TRANSACTION_ACTIONS.contains(actionCommand)) {
+                GuiPackage.getInstance().endUndoTransaction();
+            }
         }
     }
 
@@ -288,7 +300,7 @@ public final class ActionRouter implemen
     private void actionPerformed(Class<? extends Command> action, ActionEvent e, Map<String, Set<ActionListener>> actionListeners) {
         if (action != null) {
             Set<ActionListener> listenerSet = actionListeners.get(action.getName());
-            if (listenerSet != null && listenerSet.size() > 0) {
+            if (listenerSet != null && !listenerSet.isEmpty()) {
                 ActionListener[] listeners = listenerSet.toArray(new ActionListener[listenerSet.size()]);
                 for (ActionListener listener : listeners) {
                     listener.actionPerformed(e);

Modified: jmeter/trunk/src/core/org/apache/jmeter/gui/action/CheckDirty.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/gui/action/CheckDirty.java?rev=1810034&r1=1810033&r2=1810034&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/gui/action/CheckDirty.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/gui/action/CheckDirty.java Thu Sep 28 19:12:44 2017
@@ -65,11 +65,12 @@ public class CheckDirty extends Abstract
     public CheckDirty() {
         previousGuiItems = new HashMap<>();
         ActionRouter.getInstance().addPreActionListener(ExitCommand.class, this);
+        ActionRouter.getInstance().addPostActionListener(UndoCommand.class, this);
     }
 
     @Override
     public void actionPerformed(ActionEvent e) {
-        if (e.getActionCommand().equals(ActionNames.EXIT)) {
+        if (e.getActionCommand().equals(ActionNames.EXIT) || e.getActionCommand().equals(ActionNames.UNDO) || e.getActionCommand().equals(ActionNames.REDO)) {
             doAction(e);
         }
     }
@@ -110,6 +111,16 @@ public class CheckDirty extends Abstract
         // If we are merging in another test plan, we know the test plan is dirty now
         if(action.equals(ActionNames.SUB_TREE_MERGED)) {
             dirty = true;
+        } else if (action.equals(ActionNames.UNDO) || action.equals(ActionNames.REDO)) {
+            dirty = GuiPackage.getInstance().isDirty();
+            log.debug("Restoring dirty after undo/redo");
+
+            //remember
+            previousGuiItems.clear();
+            GuiPackage.getInstance().getTreeModel().getTestPlan().traverse(this);
+            if (isWorkbenchSaveable()) {
+                GuiPackage.getInstance().getTreeModel().getWorkBench().traverse(this);
+            }
         }
         else {
             dirty = false;

Modified: jmeter/trunk/src/core/org/apache/jmeter/gui/action/ResetSearchCommand.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/gui/action/ResetSearchCommand.java?rev=1810034&r1=1810033&r2=1810034&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/gui/action/ResetSearchCommand.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/gui/action/ResetSearchCommand.java Thu Sep 28 19:12:44 2017
@@ -45,14 +45,19 @@ public class ResetSearchCommand extends
     @Override
     public void doAction(ActionEvent e) {
         GuiPackage guiPackage = GuiPackage.getInstance();
-        JMeterTreeModel jMeterTreeModel = guiPackage.getTreeModel();
-        for (JMeterTreeNode jMeterTreeNode : jMeterTreeModel.getNodesOfType(Searchable.class)) {
-            if (jMeterTreeNode.getUserObject() instanceof Searchable){
-                List<JMeterTreeNode> matchingNodes = jMeterTreeNode.getPathToThreadGroup();
-                for (JMeterTreeNode jMeterTreeNode2 : matchingNodes) {
-                    jMeterTreeNode2.setMarkedBySearch(false); 
+        try{
+            guiPackage.beginUndoTransaction();
+            JMeterTreeModel jMeterTreeModel = guiPackage.getTreeModel();
+            for (JMeterTreeNode jMeterTreeNode : jMeterTreeModel.getNodesOfType(Searchable.class)) {
+                if (jMeterTreeNode.getUserObject() instanceof Searchable){
+                    List<JMeterTreeNode> matchingNodes = jMeterTreeNode.getPathToThreadGroup();
+                    for (JMeterTreeNode jMeterTreeNode2 : matchingNodes) {
+                        jMeterTreeNode2.setMarkedBySearch(false); 
+                    }
                 }
             }
+        } finally {
+            guiPackage.endUndoTransaction();
         }
         GuiPackage.getInstance().getMainFrame().repaint();
     }

Modified: jmeter/trunk/src/core/org/apache/jmeter/gui/action/SearchTreeDialog.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/gui/action/SearchTreeDialog.java?rev=1810034&r1=1810033&r2=1810034&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/gui/action/SearchTreeDialog.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/gui/action/SearchTreeDialog.java Thu Sep 28 19:12:44 2017
@@ -96,8 +96,6 @@ public class SearchTreeDialog extends JD
 
     private JButton searchAndExpandButton;
 
-    private JButton replaceButton;
-
     public SearchTreeDialog() {
         super((JFrame) null, JMeterUtils.getResString("search_tree_title"), false); //$NON-NLS-1$
         init();
@@ -170,7 +168,7 @@ public class SearchTreeDialog extends JD
         searchButton.addActionListener(this);
         searchAndExpandButton = new JButton(JMeterUtils.getResString("search_expand")); //$NON-NLS-1$
         searchAndExpandButton.addActionListener(this);
-        replaceButton = new JButton(JMeterUtils.getResString("search_replace_all")); //$NON-NLS-1$
+        JButton replaceButton = new JButton(JMeterUtils.getResString("search_replace_all")); //$NON-NLS-1$
         replaceButton.addActionListener(this);
         cancelButton = new JButton(JMeterUtils.getResString("cancel")); //$NON-NLS-1$
         cancelButton.addActionListener(this);
@@ -232,31 +230,36 @@ public class SearchTreeDialog extends JD
             searcher = new RawTextSearcher(isCaseSensitiveCB.isSelected(), searchTF.getText());
         }
         GuiPackage guiPackage = GuiPackage.getInstance();
-        JMeterTreeModel jMeterTreeModel = guiPackage.getTreeModel();
-        Set<JMeterTreeNode> nodes = new HashSet<>();
         int numberOfMatches = 0;
-        for (JMeterTreeNode jMeterTreeNode : jMeterTreeModel.getNodesOfType(Searchable.class)) {
-            try {
-                Searchable searchable = (Searchable) jMeterTreeNode.getUserObject();
-                List<JMeterTreeNode> matchingNodes = jMeterTreeNode.getPathToThreadGroup();
-                List<String> searchableTokens = searchable.getSearchableTokens();
-                boolean result = searcher.search(searchableTokens);
-                if (result) {
-                    numberOfMatches++;
-                    nodes.addAll(matchingNodes);
+        try {
+            guiPackage.beginUndoTransaction();
+            JMeterTreeModel jMeterTreeModel = guiPackage.getTreeModel();
+            Set<JMeterTreeNode> nodes = new HashSet<>();
+            for (JMeterTreeNode jMeterTreeNode : jMeterTreeModel.getNodesOfType(Searchable.class)) {
+                try {
+                    Searchable searchable = (Searchable) jMeterTreeNode.getUserObject();
+                    List<JMeterTreeNode> matchingNodes = jMeterTreeNode.getPathToThreadGroup();
+                    List<String> searchableTokens = searchable.getSearchableTokens();
+                    boolean result = searcher.search(searchableTokens);
+                    if (result) {
+                        numberOfMatches++;
+                        nodes.addAll(matchingNodes);
+                    }
+                } catch (Exception ex) {
+                    logger.error("Error occurred searching for word:"+ wordToSearch+ " in node:"+jMeterTreeNode.getName(), ex);
                 }
-            } catch (Exception ex) {
-                logger.error("Error occurred searching for word:"+ wordToSearch+ " in node:"+jMeterTreeNode.getName(), ex);
             }
-        }
-        GuiPackage guiInstance = GuiPackage.getInstance();
-        JTree jTree = guiInstance.getMainFrame().getTree();
-
-        for (JMeterTreeNode jMeterTreeNode : nodes) {
-            jMeterTreeNode.setMarkedBySearch(true);
-            if (expand) {
-                jTree.expandPath(new TreePath(jMeterTreeNode.getPath()));
+            GuiPackage guiInstance = GuiPackage.getInstance();
+            JTree jTree = guiInstance.getMainFrame().getTree();
+    
+            for (JMeterTreeNode jMeterTreeNode : nodes) {
+                jMeterTreeNode.setMarkedBySearch(true);
+                if (expand) {
+                    jTree.expandPath(new TreePath(jMeterTreeNode.getPath()));
+                }
             }
+        } finally {
+            guiPackage.endUndoTransaction();
         }
         GuiPackage.getInstance().getMainFrame().repaint();
         searchTF.requestFocusInWindow();

Modified: jmeter/trunk/src/core/org/apache/jmeter/gui/action/UndoCommand.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/gui/action/UndoCommand.java?rev=1810034&r1=1810033&r2=1810034&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/gui/action/UndoCommand.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/gui/action/UndoCommand.java Thu Sep 28 19:12:44 2017
@@ -46,9 +46,9 @@ public class UndoCommand extends Abstrac
         final String command = e.getActionCommand();
 
         if (command.equals(ActionNames.UNDO)) {
-            guiPackage.goInHistory(-1);
+            guiPackage.undo();
         } else if (command.equals(ActionNames.REDO)) {
-            guiPackage.goInHistory(1);
+            guiPackage.redo();
         } else {
             throw new IllegalArgumentException("Wrong action called: " + command);
         }

Modified: jmeter/trunk/src/core/org/apache/jmeter/gui/tree/JMeterTreeNode.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/gui/tree/JMeterTreeNode.java?rev=1810034&r1=1810033&r2=1810034&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/gui/tree/JMeterTreeNode.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/gui/tree/JMeterTreeNode.java Thu Sep 28 19:12:44 2017
@@ -95,6 +95,9 @@ public class JMeterTreeNode extends Defa
      * @param tagged The flag to be used for tagging
      */
     public void setMarkedBySearch(boolean tagged) {
+        if(this.markedBySearch == tagged) {
+            return;
+        }
         this.markedBySearch = tagged;
         treeModel.nodeChanged(this);
     }