This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 24274a9574 Switch pipeline and workflow undo/redo to XML snapshots,
fixes #8037 (#8042)
24274a9574 is described below
commit 24274a957447a0b08e337b2515dec90dc19dff34
Author: Matt Casters <[email protected]>
AuthorDate: Fri Aug 21 09:28:48 2026 +0200
Switch pipeline and workflow undo/redo to XML snapshots, fixes #8037 (#8042)
Replace incremental ChangeAction replay with gzip-compressed document
snapshots so undo/redo restores the whole pipeline or workflow instead of
puzzling typed deltas back together. TableView cell undo is unchanged.
Add a Maximum undo operations field on the Explorer Perspective options
tab that reads and writes PropsUi.getMaxUndo().
---
.../org/apache/hop/core/undo/XmlSnapshotUndo.java | 227 +++++++++++
.../java/org/apache/hop/pipeline/PipelineMeta.java | 17 +
.../java/org/apache/hop/workflow/WorkflowMeta.java | 17 +
.../apache/hop/core/undo/XmlSnapshotUndoTest.java | 211 ++++++++++
.../main/java/org/apache/hop/ui/hopgui/HopGui.java | 59 ++-
.../ui/hopgui/delegates/HopGuiUndoDelegate.java | 28 +-
.../file/delegates/HopGuiNotePadDelegate.java | 39 +-
.../hopgui/file/pipeline/HopGuiPipelineGraph.java | 439 ++++++++++-----------
.../delegates/HopGuiPipelineClipboardDelegate.java | 22 +-
.../delegates/HopGuiPipelineTransformDelegate.java | 21 +-
.../delegates/HopGuiPipelineUndoDelegate.java | 306 +-------------
.../ui/hopgui/file/shared/HopGuiAbstractGraph.java | 12 +
.../file/shared/HopGuiGraphSnapshotUndo.java | 218 ++++++++++
.../hopgui/file/shared/ISnapshotUndoSupport.java | 46 +++
.../hopgui/file/workflow/HopGuiWorkflowGraph.java | 218 +++++-----
.../delegates/HopGuiWorkflowActionDelegate.java | 10 +-
.../delegates/HopGuiWorkflowClipboardDelegate.java | 19 +-
.../delegates/HopGuiWorkflowUndoDelegate.java | 304 +-------------
.../config/ExplorerPerspectiveConfigPlugin.java | 45 +++
.../ui/hopgui/messages/messages_en_US.properties | 4 +
.../config/messages/messages_en_US.properties | 3 +
21 files changed, 1228 insertions(+), 1037 deletions(-)
diff --git a/engine/src/main/java/org/apache/hop/core/undo/XmlSnapshotUndo.java
b/engine/src/main/java/org/apache/hop/core/undo/XmlSnapshotUndo.java
new file mode 100644
index 0000000000..80575c6563
--- /dev/null
+++ b/engine/src/main/java/org/apache/hop/core/undo/XmlSnapshotUndo.java
@@ -0,0 +1,227 @@
+/*
+ * 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.hop.core.undo;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.IntSupplier;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.serializer.xml.XmlMetadataUtil;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+/**
+ * Gzip-compressed XML snapshots of a metadata document for undo/redo.
+ *
+ * <p>Unlike {@link ChangeAction}, this stores the whole document so restore
cannot miss a field.
+ * Capture uses {@link XmlMetadataUtil#serializeObjectToXml(Object)} (no
license header, no
+ * formatter) so snapshots stay small and stable.
+ */
+public class XmlSnapshotUndo<M> {
+
+ @FunctionalInterface
+ public interface ContentRestorer<M> {
+ void restore(M target, Node xmlRoot, IHopMetadataProvider
metadataProvider, String filename)
+ throws HopException;
+ }
+
+ private final Class<M> modelClass;
+ private final String xmlRootTag;
+ private final ContentRestorer<M> restorer;
+ private final IntSupplier maxUndo;
+
+ private final List<byte[]> undoStack = new ArrayList<>();
+ private final List<byte[]> redoStack = new ArrayList<>();
+ private boolean applyingSnapshot;
+
+ public XmlSnapshotUndo(
+ Class<M> modelClass, String xmlRootTag, ContentRestorer<M> restorer,
IntSupplier maxUndo) {
+ this.modelClass = modelClass;
+ this.xmlRootTag = xmlRootTag;
+ this.restorer = restorer;
+ this.maxUndo = maxUndo != null ? maxUndo : () -> Const.MAX_UNDO;
+ }
+
+ public void clear() {
+ undoStack.clear();
+ redoStack.clear();
+ }
+
+ public boolean canUndo() {
+ return !undoStack.isEmpty();
+ }
+
+ public boolean canRedo() {
+ return !redoStack.isEmpty();
+ }
+
+ public boolean isApplyingSnapshot() {
+ return applyingSnapshot;
+ }
+
+ public int getUndoSize() {
+ return undoStack.size();
+ }
+
+ public int getRedoSize() {
+ return redoStack.size();
+ }
+
+ public void markChange(M model, IHopMetadataProvider metadataProvider)
throws HopException {
+ if (applyingSnapshot || model == null) {
+ return;
+ }
+ pushSnapshot(captureSnapshot(model, metadataProvider));
+ }
+
+ public void pushSnapshot(byte[] snapshot) {
+ if (applyingSnapshot || snapshot == null) {
+ return;
+ }
+ undoStack.add(snapshot);
+ trimStack(undoStack);
+ redoStack.clear();
+ }
+
+ public byte[] captureSnapshot(M model, IHopMetadataProvider metadataProvider)
+ throws HopException {
+ if (model == null) {
+ throw new HopException("Cannot capture snapshot of a null model");
+ }
+ try {
+ String xml = XmlHandler.aroundTag(xmlRootTag,
XmlMetadataUtil.serializeObjectToXml(model));
+ return compress(xml);
+ } catch (Exception e) {
+ throw new HopException("Error capturing " + modelClass.getSimpleName() +
" snapshot", e);
+ }
+ }
+
+ /**
+ * Restore the previous snapshot into {@code current}. The live document is
pushed onto the redo
+ * stack first.
+ *
+ * @return {@code true} if a snapshot was applied
+ */
+ public boolean undo(M current, IHopMetadataProvider metadataProvider, String
filename)
+ throws HopException {
+ if (!canUndo() || current == null) {
+ return false;
+ }
+ applyingSnapshot = true;
+ try {
+ redoStack.add(captureSnapshot(current, metadataProvider));
+ trimStack(redoStack);
+ byte[] previous = undoStack.remove(undoStack.size() - 1);
+ restoreInto(previous, current, metadataProvider, filename);
+ return true;
+ } finally {
+ applyingSnapshot = false;
+ }
+ }
+
+ /**
+ * Restore the next redo snapshot into {@code current}. The live document is
pushed onto the undo
+ * stack first.
+ *
+ * @return {@code true} if a snapshot was applied
+ */
+ public boolean redo(M current, IHopMetadataProvider metadataProvider, String
filename)
+ throws HopException {
+ if (!canRedo() || current == null) {
+ return false;
+ }
+ applyingSnapshot = true;
+ try {
+ undoStack.add(captureSnapshot(current, metadataProvider));
+ trimStack(undoStack);
+ byte[] next = redoStack.remove(redoStack.size() - 1);
+ restoreInto(next, current, metadataProvider, filename);
+ return true;
+ } finally {
+ applyingSnapshot = false;
+ }
+ }
+
+ public void restoreInto(
+ byte[] snapshot, M target, IHopMetadataProvider metadataProvider, String
filename)
+ throws HopException {
+ try {
+ String xml = decompress(snapshot);
+ Document document = XmlHandler.loadXmlString(xml);
+ Node rootNode = XmlHandler.getSubNode(document, xmlRootTag);
+ if (rootNode == null) {
+ rootNode = document.getDocumentElement();
+ }
+ restorer.restore(target, rootNode, metadataProvider, filename);
+ } catch (Exception e) {
+ throw new HopException("Error restoring " + modelClass.getSimpleName() +
" snapshot", e);
+ }
+ }
+
+ /**
+ * Gzip headers include a timestamp, so compressed bytes of identical XML
are not equal. Compare
+ * inflated XML instead.
+ */
+ public static boolean sameXmlContent(byte[] left, byte[] right) {
+ if (left == right) {
+ return true;
+ }
+ if (left == null || right == null) {
+ return false;
+ }
+ try {
+ return decompress(left).equals(decompress(right));
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ static byte[] compress(String xml) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(xml.length());
+ try (GZIPOutputStream gzip = new GZIPOutputStream(baos)) {
+ gzip.write(xml.getBytes(StandardCharsets.UTF_8));
+ }
+ return baos.toByteArray();
+ }
+
+ static String decompress(byte[] compressed) throws IOException {
+ try (GZIPInputStream gzip = new GZIPInputStream(new
ByteArrayInputStream(compressed));
+ ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ gzip.transferTo(baos);
+ return baos.toString(StandardCharsets.UTF_8);
+ }
+ }
+
+ private void trimStack(List<byte[]> stack) {
+ int max = maxUndo.getAsInt();
+ if (max < 1) {
+ max = 1;
+ }
+ while (stack.size() > max) {
+ stack.remove(0);
+ }
+ }
+}
diff --git a/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java
b/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java
index 4546493d80..d50f408754 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java
@@ -1621,6 +1621,23 @@ public class PipelineMeta extends AbstractMeta
clearChanged();
}
+ /**
+ * Replace this pipeline's persisted content from a snapshot XML node. Used
by GUI undo/redo.
+ *
+ * <p>Does not fire {@code PipelineMetaLoaded} (undo is not a file open) and
does not call {@link
+ * #clearChanged()} — the caller decides the dirty flag.
+ */
+ public void restoreContentFromXml(
+ Node pipelineNode, String filename, IHopMetadataProvider
metadataProvider)
+ throws HopException {
+ this.metadataProvider = metadataProvider;
+ clear();
+ setFilename(filename);
+ XmlMetadataUtil.deSerializeFromXml(
+ null, null, pipelineNode, PipelineMeta.class, this, metadataProvider);
+ lookupReferencesAfterLoading();
+ }
+
private void deSerializeXml(
Node pipelineNode,
String filename,
diff --git a/engine/src/main/java/org/apache/hop/workflow/WorkflowMeta.java
b/engine/src/main/java/org/apache/hop/workflow/WorkflowMeta.java
index e21f72ac4b..854adda4d6 100644
--- a/engine/src/main/java/org/apache/hop/workflow/WorkflowMeta.java
+++ b/engine/src/main/java/org/apache/hop/workflow/WorkflowMeta.java
@@ -448,6 +448,23 @@ public class WorkflowMeta extends AbstractMeta
}
}
+ /**
+ * Replace this workflow's persisted content from a snapshot XML node. Used
by GUI undo/redo.
+ *
+ * <p>Does not fire {@code WorkflowMetaLoaded} (undo is not a file open) and
does not call {@link
+ * #clearChanged()} — the caller decides the dirty flag.
+ */
+ public void restoreContentFromXml(
+ Node workflowNode, String filename, IHopMetadataProvider
metadataProvider)
+ throws HopException {
+ this.metadataProvider = metadataProvider;
+ clear();
+ setFilename(filename);
+ XmlMetadataUtil.deSerializeFromXml(
+ null, null, workflowNode, WorkflowMeta.class, this, metadataProvider);
+ lookupReferencesAfterLoading();
+ }
+
/**
* After loading there can still be some references to other transforms or
indeed this pipeline
* that need to be set. This is happening here.
diff --git
a/engine/src/test/java/org/apache/hop/core/undo/XmlSnapshotUndoTest.java
b/engine/src/test/java/org/apache/hop/core/undo/XmlSnapshotUndoTest.java
new file mode 100644
index 0000000000..53864cfdef
--- /dev/null
+++ b/engine/src/test/java/org/apache/hop/core/undo/XmlSnapshotUndoTest.java
@@ -0,0 +1,211 @@
+/*
+ * 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.hop.core.undo;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.NotePadMeta;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.PipelineHopMeta;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.dummy.DummyMeta;
+import org.apache.hop.workflow.WorkflowHopMeta;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.apache.hop.workflow.action.ActionMeta;
+import org.apache.hop.workflow.actions.dummy.ActionDummy;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class XmlSnapshotUndoTest {
+
+ private final MemoryMetadataProvider metadataProvider = new
MemoryMetadataProvider();
+
+ @BeforeAll
+ static void setUpBeforeClass() throws Exception {
+ HopEnvironment.init();
+ }
+
+ @Test
+ void pipelineRoundTripUndoRedoAndRedoClearedOnNewChange() throws Exception {
+ PipelineMeta pipelineMeta = samplePipeline("before");
+ pipelineMeta.setNameSynchronizedWithFilename(false);
+ pipelineMeta.setFilename("/tmp/sample.hpl");
+ XmlSnapshotUndo<PipelineMeta> undo = pipelineUndo(10);
+
+ undo.markChange(pipelineMeta, metadataProvider);
+ pipelineMeta.setName("after");
+ pipelineMeta.getTransform(0).setLocation(200, 200);
+
+ assertTrue(undo.canUndo());
+ assertFalse(undo.canRedo());
+
+ assertTrue(undo.undo(pipelineMeta, metadataProvider, "/tmp/sample.hpl"));
+ assertEquals("before", pipelineMeta.getName());
+ assertEquals(50, pipelineMeta.getTransform(0).getLocation().x);
+ assertEquals("/tmp/sample.hpl", pipelineMeta.getFilename());
+ assertEquals("A", pipelineMeta.getTransform(0).getName());
+ assertEquals("B", pipelineMeta.getTransform(1).getName());
+ assertEquals("A",
pipelineMeta.getPipelineHop(0).getFromTransform().getName());
+ assertEquals("B",
pipelineMeta.getPipelineHop(0).getToTransform().getName());
+ assertEquals("note", pipelineMeta.getNote(0).getNote());
+ assertTrue(undo.canRedo());
+
+ assertTrue(undo.redo(pipelineMeta, metadataProvider, "/tmp/sample.hpl"));
+ assertEquals("after", pipelineMeta.getName());
+ assertEquals(200, pipelineMeta.getTransform(0).getLocation().x);
+ assertEquals("/tmp/sample.hpl", pipelineMeta.getFilename());
+
+ undo.markChange(pipelineMeta, metadataProvider);
+ pipelineMeta.setName("newer");
+ assertFalse(undo.canRedo());
+ assertTrue(undo.undo(pipelineMeta, metadataProvider, "/tmp/sample.hpl"));
+ assertEquals("after", pipelineMeta.getName());
+ }
+
+ @Test
+ void workflowRoundTripRestoresHopsAndNotes() throws Exception {
+ WorkflowMeta workflowMeta = sampleWorkflow("wf-before");
+ workflowMeta.setNameSynchronizedWithFilename(false);
+ workflowMeta.setFilename("/tmp/sample.hwf");
+ XmlSnapshotUndo<WorkflowMeta> undo = workflowUndo(10);
+
+ undo.markChange(workflowMeta, metadataProvider);
+ workflowMeta.setName("wf-after");
+ workflowMeta.removeWorkflowHop(0);
+
+ assertTrue(undo.undo(workflowMeta, metadataProvider, "/tmp/sample.hwf"));
+ assertEquals("wf-before", workflowMeta.getName());
+ assertEquals(1, workflowMeta.nrWorkflowHops());
+ assertEquals("start",
workflowMeta.getWorkflowHop(0).getFromAction().getName());
+ assertEquals("dummy",
workflowMeta.getWorkflowHop(0).getToAction().getName());
+ assertEquals("hello", workflowMeta.getNote(0).getNote());
+ assertEquals("/tmp/sample.hwf", workflowMeta.getFilename());
+ }
+
+ @Test
+ void trimHonorsMaxUndo() throws Exception {
+ PipelineMeta pipelineMeta = samplePipeline("v0");
+ XmlSnapshotUndo<PipelineMeta> undo = pipelineUndo(2);
+
+ undo.markChange(pipelineMeta, metadataProvider);
+ pipelineMeta.setName("v1");
+ undo.markChange(pipelineMeta, metadataProvider);
+ pipelineMeta.setName("v2");
+ undo.markChange(pipelineMeta, metadataProvider);
+ pipelineMeta.setName("v3");
+
+ assertEquals(2, undo.getUndoSize());
+ assertTrue(undo.undo(pipelineMeta, metadataProvider, null));
+ assertEquals("v2", pipelineMeta.getName());
+ assertTrue(undo.undo(pipelineMeta, metadataProvider, null));
+ assertEquals("v1", pipelineMeta.getName());
+ assertFalse(undo.undo(pipelineMeta, metadataProvider, null));
+ }
+
+ @Test
+ void applyingSnapshotDoesNotRecord() throws Exception {
+ PipelineMeta pipelineMeta = samplePipeline("orig");
+ AtomicInteger restoreCalls = new AtomicInteger();
+ XmlSnapshotUndo<PipelineMeta> undo =
+ new XmlSnapshotUndo<>(
+ PipelineMeta.class,
+ PipelineMeta.XML_TAG,
+ (target, node, provider, filename) -> {
+ restoreCalls.incrementAndGet();
+ target.restoreContentFromXml(node, filename, provider);
+ },
+ () -> 10);
+
+ undo.markChange(pipelineMeta, metadataProvider);
+ pipelineMeta.setName("changed");
+ assertTrue(undo.undo(pipelineMeta, metadataProvider, null));
+ assertEquals(1, restoreCalls.get());
+ // Nested mark during restore is skipped because applyingSnapshot is true.
+ assertEquals(1, undo.getRedoSize());
+ assertEquals(0, undo.getUndoSize());
+ }
+
+ @Test
+ void sameXmlContentIgnoresGzipHeaderTimestamp() throws Exception {
+ PipelineMeta pipelineMeta = samplePipeline("same");
+ XmlSnapshotUndo<PipelineMeta> undo = pipelineUndo(5);
+ byte[] first = undo.captureSnapshot(pipelineMeta, metadataProvider);
+ byte[] second = undo.captureSnapshot(pipelineMeta, metadataProvider);
+ assertTrue(XmlSnapshotUndo.sameXmlContent(first, second));
+ }
+
+ @Test
+ void restoreDoesNotRequireClone() throws Exception {
+ PipelineMeta pipelineMeta = samplePipeline("clone-free");
+ XmlSnapshotUndo<PipelineMeta> undo = pipelineUndo(5);
+ undo.markChange(pipelineMeta, metadataProvider);
+ pipelineMeta.addTransform(new TransformMeta("C", new DummyMeta()));
+ assertTrue(undo.undo(pipelineMeta, metadataProvider, null));
+ assertEquals(2, pipelineMeta.nrTransforms());
+ }
+
+ private static XmlSnapshotUndo<PipelineMeta> pipelineUndo(int max) {
+ return new XmlSnapshotUndo<>(
+ PipelineMeta.class,
+ PipelineMeta.XML_TAG,
+ (target, node, provider, filename) ->
+ target.restoreContentFromXml(node, filename, provider),
+ () -> max);
+ }
+
+ private static XmlSnapshotUndo<WorkflowMeta> workflowUndo(int max) {
+ return new XmlSnapshotUndo<>(
+ WorkflowMeta.class,
+ WorkflowMeta.XML_TAG,
+ (target, node, provider, filename) ->
+ target.restoreContentFromXml(node, filename, provider),
+ () -> max);
+ }
+
+ private static PipelineMeta samplePipeline(String name) {
+ PipelineMeta pipelineMeta = new PipelineMeta();
+ pipelineMeta.setName(name);
+ TransformMeta a = new TransformMeta("A", new DummyMeta());
+ a.setLocation(50, 50);
+ TransformMeta b = new TransformMeta("B", new DummyMeta());
+ b.setLocation(150, 50);
+ pipelineMeta.addTransform(a);
+ pipelineMeta.addTransform(b);
+ pipelineMeta.addPipelineHop(new PipelineHopMeta(a, b));
+ pipelineMeta.addNote(new NotePadMeta("note", 10, 10, 80, 40));
+ return pipelineMeta;
+ }
+
+ private static WorkflowMeta sampleWorkflow(String name) {
+ WorkflowMeta workflowMeta = new WorkflowMeta();
+ workflowMeta.setName(name);
+ ActionMeta start = new ActionMeta(new ActionDummy("start"));
+ start.setLocation(50, 50);
+ ActionMeta dummy = new ActionMeta(new ActionDummy("dummy"));
+ dummy.setLocation(150, 50);
+ workflowMeta.addAction(start);
+ workflowMeta.addAction(dummy);
+ workflowMeta.addWorkflowHop(new WorkflowHopMeta(start, dummy));
+ workflowMeta.addNote(new NotePadMeta("hello", 10, 10, 80, 40));
+ return workflowMeta;
+ }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
index 29b3a32cd7..4aa4225097 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
@@ -119,6 +119,7 @@ import org.apache.hop.ui.hopgui.file.IHopFileType;
import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
import org.apache.hop.ui.hopgui.file.empty.EmptyFileType;
import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
+import org.apache.hop.ui.hopgui.file.shared.ISnapshotUndoSupport;
import org.apache.hop.ui.hopgui.file.workflow.HopGuiWorkflowGraph;
import org.apache.hop.ui.hopgui.perspective.EmptyHopPerspective;
import org.apache.hop.ui.hopgui.perspective.HopPerspectiveManager;
@@ -2053,40 +2054,60 @@ public class HopGui
}
public void setUndoMenu(IUndo undoInterface) {
- // Grab the undo and redo menu items...
- //
- MenuItem undoItem = mainMenuWidgets.findMenuItem(ID_MAIN_MENU_EDIT_UNDO);
- MenuItem redoItem = mainMenuWidgets.findMenuItem(ID_MAIN_MENU_EDIT_REDO);
- if (undoItem == null || redoItem == null || undoItem.isDisposed() ||
redoItem.isDisposed()) {
- return;
+ try {
+ IHopFileTypeHandler handler = getActiveFileTypeHandler();
+ if (handler instanceof ISnapshotUndoSupport support
+ && (undoInterface == null || support.isUndoMeta(undoInterface))) {
+ setUndoMenu(support.canUndo(), support.canRedo());
+ return;
+ }
+ } catch (Exception e) {
+ // Menu is built before a file handler exists.
}
- ChangeAction prev = null;
- ChangeAction next = null;
+ ChangeAction prev = undoInterface != null ? undoInterface.viewThisUndo() :
null;
+ ChangeAction next = undoInterface != null ? undoInterface.viewNextUndo() :
null;
+ setUndoMenuItems(prev != null, next != null, prev, next);
+ }
- if (undoInterface != null) {
- prev = undoInterface.viewThisUndo();
- next = undoInterface.viewNextUndo();
+ public void setUndoMenu(boolean canUndo, boolean canRedo) {
+ setUndoMenuItems(canUndo, canRedo, null, null);
+ }
+
+ private void setUndoMenuItems(
+ boolean canUndo, boolean canRedo, ChangeAction prev, ChangeAction next) {
+ GuiMenuWidgets widgets = getMainMenuWidgets();
+ if (widgets == null) {
+ return;
+ }
+ MenuItem undoItem = widgets.findMenuItem(ID_MAIN_MENU_EDIT_UNDO);
+ MenuItem redoItem = widgets.findMenuItem(ID_MAIN_MENU_EDIT_REDO);
+ if (undoItem == null || redoItem == null || undoItem.isDisposed() ||
redoItem.isDisposed()) {
+ return;
}
- undoItem.setEnabled(prev != null);
- if (prev == null) {
+ undoItem.setEnabled(canUndo);
+ if (!canUndo) {
undoItem.setText(UNDO_UNAVAILABLE);
- } else {
+ } else if (prev != null) {
undoItem.setText(BaseMessages.getString(PKG,
"HopGui.Menu.Undo.Available", prev.toString()));
+ } else {
+ undoItem.setText(BaseMessages.getString(PKG, "HopGui.Menu.Edit.Undo"));
}
- KeyboardShortcut undoShortcut =
mainMenuWidgets.findKeyboardShortcut(ID_MAIN_MENU_EDIT_UNDO);
+ KeyboardShortcut undoShortcut =
widgets.findKeyboardShortcut(ID_MAIN_MENU_EDIT_UNDO);
if (undoShortcut != null) {
GuiMenuWidgets.appendShortCut(undoItem, undoShortcut);
}
- redoItem.setEnabled(next != null);
- if (next == null) {
+ redoItem.setEnabled(canRedo);
+ if (!canRedo) {
redoItem.setText(REDO_UNAVAILABLE);
- } else {
+ } else if (next != null) {
redoItem.setText(BaseMessages.getString(PKG,
"HopGui.Menu.Redo.Available", next.toString()));
+ } else {
+ redoItem.setText(BaseMessages.getString(PKG, "HopGui.Menu.Edit.Redo"));
}
- KeyboardShortcut redoShortcut =
mainMenuWidgets.findKeyboardShortcut(ID_MAIN_MENU_EDIT_REDO);
+ KeyboardShortcut redoShortcut =
widgets.findKeyboardShortcut(ID_MAIN_MENU_EDIT_REDO);
if (redoShortcut != null) {
GuiMenuWidgets.appendShortCut(redoItem, redoShortcut);
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiUndoDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiUndoDelegate.java
index acf149dc40..b8c4e46ec2 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiUndoDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiUndoDelegate.java
@@ -22,6 +22,8 @@ import org.apache.hop.core.IAddUndoPosition;
import org.apache.hop.core.gui.IUndo;
import org.apache.hop.core.gui.Point;
import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
+import org.apache.hop.ui.hopgui.file.shared.ISnapshotUndoSupport;
public class HopGuiUndoDelegate implements IAddUndoPosition {
private HopGui hopGui;
@@ -35,6 +37,9 @@ public class HopGuiUndoDelegate implements IAddUndoPosition {
}
public void addUndoNew(IUndo undoInterface, Object[] obj, int[] position,
boolean nextAlso) {
+ if (recordSnapshot(undoInterface, nextAlso)) {
+ return;
+ }
undoInterface.addUndo(obj, null, position, null, null,
AbstractMeta.TYPE_UNDO_NEW, nextAlso);
hopGui.setUndoMenu(undoInterface);
}
@@ -46,6 +51,9 @@ public class HopGuiUndoDelegate implements IAddUndoPosition {
// Undo delete object
public void addUndoDelete(IUndo undoInterface, Object[] obj, int[] position,
boolean nextAlso) {
+ if (recordSnapshot(undoInterface, nextAlso)) {
+ return;
+ }
undoInterface.addUndo(obj, null, position, null, null,
AbstractMeta.TYPE_UNDO_DELETE, nextAlso);
hopGui.setUndoMenu(undoInterface);
}
@@ -60,9 +68,10 @@ public class HopGuiUndoDelegate implements IAddUndoPosition {
// Change of transform, connection, hop or note...
public void addUndoPosition(
IUndo undoInterface, Object[] obj, int[] pos, Point[] prev, Point[]
curr, boolean nextAlso) {
- // It's better to store the indexes of the objects, not the objects
- // itself!
- undoInterface.addUndo(obj, null, pos, prev, curr,
AbstractMeta.TYPE_UNDO_POSITION, false);
+ if (recordSnapshot(undoInterface, nextAlso)) {
+ return;
+ }
+ undoInterface.addUndo(obj, null, pos, prev, curr,
AbstractMeta.TYPE_UNDO_POSITION, nextAlso);
hopGui.setUndoMenu(undoInterface);
}
@@ -74,10 +83,23 @@ public class HopGuiUndoDelegate implements IAddUndoPosition
{
// Change of transform, connection, hop or note...
public void addUndoChange(
IUndo undoInterface, Object[] from, Object[] to, int[] pos, boolean
nextAlso) {
+ if (recordSnapshot(undoInterface, nextAlso)) {
+ return;
+ }
undoInterface.addUndo(from, to, pos, null, null,
AbstractMeta.TYPE_UNDO_CHANGE, nextAlso);
hopGui.setUndoMenu(undoInterface);
}
+ private boolean recordSnapshot(IUndo undoInterface, boolean nextAlso) {
+ IHopFileTypeHandler handler = hopGui.getActiveFileTypeHandler();
+ if (handler instanceof ISnapshotUndoSupport support &&
support.isUndoMeta(undoInterface)) {
+ support.recordAfterChange(nextAlso);
+ hopGui.setUndoMenu(support.canUndo(), support.canRedo());
+ return true;
+ }
+ return false;
+ }
+
/**
* Gets hopGui
*
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/delegates/HopGuiNotePadDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/delegates/HopGuiNotePadDelegate.java
index a8c71e6090..0060d1a70d 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/delegates/HopGuiNotePadDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/delegates/HopGuiNotePadDelegate.java
@@ -30,6 +30,7 @@ import org.apache.hop.ui.core.security.HopSecurityUi;
import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.hopgui.dialog.NotePadDialog;
import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
+import org.apache.hop.ui.hopgui.file.shared.ISnapshotUndoSupport;
public class HopGuiNotePadDelegate {
private static final Class<?> PKG = HopGui.class;
@@ -56,17 +57,13 @@ public class HopGuiNotePadDelegate {
if (!HopSecurityUi.check(Permission.FILE_EDIT)) {
return;
}
- int[] idxs = new int[notes.size()];
- NotePadMeta[] noteCopies = new NotePadMeta[notes.size()];
- for (int i = 0; i < idxs.length; i++) {
- idxs[i] = meta.indexOfNote(notes.get(i));
- noteCopies[i] = new NotePadMeta(notes.get(i));
- }
+ markUndo(meta);
for (NotePadMeta notePadMeta : notes) {
int idx = meta.indexOfNote(notePadMeta);
- meta.removeNote(idx);
+ if (idx >= 0) {
+ meta.removeNote(idx);
+ }
}
- hopGui.undoDelegate.addUndoDelete(meta, noteCopies, idxs);
handler.updateGui();
}
@@ -76,13 +73,31 @@ public class HopGuiNotePadDelegate {
}
int idx = meta.indexOfNote(notePadMeta);
if (idx >= 0) {
+ markUndo(meta);
meta.removeNote(idx);
- hopGui.undoDelegate.addUndoDelete(
- meta, new NotePadMeta[] {(NotePadMeta) notePadMeta.clone()}, new
int[] {idx});
}
handler.updateGui();
}
+ private void markUndo(AbstractMeta meta) {
+ if (handler instanceof ISnapshotUndoSupport support &&
support.isUndoMeta(meta)) {
+ support.markUndoPoint();
+ }
+ }
+
+ private byte[] captureUndo(AbstractMeta meta) {
+ if (handler instanceof ISnapshotUndoSupport support &&
support.isUndoMeta(meta)) {
+ return support.captureUndoSnapshot();
+ }
+ return null;
+ }
+
+ private void commitUndo(AbstractMeta meta, byte[] beforeSnapshot) {
+ if (handler instanceof ISnapshotUndoSupport support &&
support.isUndoMeta(meta)) {
+ support.commitDialogUndo(beforeSnapshot);
+ }
+ }
+
public void newNote(IVariables variables, AbstractMeta meta, int x, int y) {
if (!HopSecurityUi.check(Permission.FILE_EDIT)) {
return;
@@ -90,6 +105,7 @@ public class HopGuiNotePadDelegate {
String title = BaseMessages.getString(PKG,
"PipelineGraph.Dialog.NoteEditor.Title");
NotePadDialog dialog =
new NotePadDialog(variables, hopGui.getShell(), title,
meta.getFilename());
+ byte[] beforeSnapshot = captureUndo(meta);
NotePadMeta note = dialog.open();
if (note != null) {
NotePadMeta newNote =
@@ -117,8 +133,7 @@ public class HopGuiNotePadDelegate {
// Apply grid snapping; default width is readable for Markdown wrapping
PropsUi.setSize(newNote, defaultNoteWidth(), ConstUi.NOTE_MIN_SIZE);
meta.addNote(newNote);
- hopGui.undoDelegate.addUndoNew(
- meta, new NotePadMeta[] {newNote}, new int[]
{meta.indexOfNote(newNote)});
+ commitUndo(meta, beforeSnapshot);
handler.updateGui();
}
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
index e5e56bc139..eda1c64c9c 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
@@ -17,6 +17,8 @@
package org.apache.hop.ui.hopgui.file.pipeline;
+import static java.lang.Thread.sleep;
+
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
@@ -61,6 +63,7 @@ import org.apache.hop.core.gui.CanvasSvgRenderResult;
import org.apache.hop.core.gui.DPoint;
import org.apache.hop.core.gui.IGc;
import org.apache.hop.core.gui.IRedrawable;
+import org.apache.hop.core.gui.IUndo;
import org.apache.hop.core.gui.Point;
import org.apache.hop.core.gui.Rectangle;
import org.apache.hop.core.gui.SnapAllignDistribute;
@@ -189,7 +192,9 @@ import
org.apache.hop.ui.hopgui.file.pipeline.extension.HopGuiPipelineGraphExten
import
org.apache.hop.ui.hopgui.file.pipeline.extension.PipelineRenamedExtension;
import org.apache.hop.ui.hopgui.file.shared.DrillDownGuiPlugin;
import org.apache.hop.ui.hopgui.file.shared.HopGuiAbstractGraph;
+import org.apache.hop.ui.hopgui.file.shared.HopGuiGraphSnapshotUndo;
import org.apache.hop.ui.hopgui.file.shared.HopGuiTooltipExtension;
+import org.apache.hop.ui.hopgui.file.shared.ISnapshotUndoSupport;
import org.apache.hop.ui.hopgui.file.shared.PipelineRowSamplerHelper;
import org.apache.hop.ui.hopgui.perspective.execution.ExecutionPerspective;
import org.apache.hop.ui.hopgui.perspective.execution.IExecutionViewer;
@@ -252,7 +257,8 @@ public class HopGuiPipelineGraph extends HopGuiAbstractGraph
ILogParentProvided,
IHopFileTypeHandler,
IGuiRefresher,
- IWebCanvasGraph {
+ IWebCanvasGraph,
+ ISnapshotUndoSupport {
private static final Class<?> PKG = HopGui.class;
@@ -441,6 +447,7 @@ public class HopGuiPipelineGraph extends HopGuiAbstractGraph
public HopGuiPipelineClipboardDelegate pipelineClipboardDelegate;
public HopGuiPipelineHopDelegate pipelineHopDelegate;
public HopGuiPipelineUndoDelegate pipelineUndoDelegate;
+ private final HopGuiGraphSnapshotUndo<PipelineMeta> snapshotUndo;
public HopGuiServerDelegate serverDelegate;
public HopGuiNotePadDelegate notePadDelegate;
@@ -522,6 +529,17 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
pipelineTransformDelegate = new HopGuiPipelineTransformDelegate(hopGui,
this);
pipelineHopDelegate = new HopGuiPipelineHopDelegate(hopGui, this);
pipelineUndoDelegate = new HopGuiPipelineUndoDelegate(hopGui, this);
+ snapshotUndo =
+ new HopGuiGraphSnapshotUndo<>(
+ hopGui,
+ PipelineMeta.class,
+ PipelineMeta.XML_TAG,
+ (target, node, provider, filename) ->
+ target.restoreContentFromXml(node, filename, provider),
+ () -> this.pipelineMeta,
+ this::getFilename,
+ this::restoreAfterSnapshot);
+ snapshotUndo.initialize();
pipelineRunDelegate = new HopGuiPipelineRunDelegate(hopGui, this);
serverDelegate = new HopGuiServerDelegate(hopGui, this);
@@ -952,6 +970,7 @@ public class HopGuiPipelineGraph extends HopGuiAbstractGraph
// native SWT keeps the threshold behaviour to distinguish a click
from a drag.
if (EnvironmentUtils.getInstance().isWeb() && event.button == 1 &&
!shift && !control) {
iconDragCommitted = true;
+ markPositionUndoPoint();
dragSelection = true;
canvas.setData("mode", "drag");
selectedTransforms = pipelineMeta.getSelectedTransforms();
@@ -1316,33 +1335,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
pipelineGridDelegate.onPipelineSelectionChanged();
}
- // We moved around some items: store undo info...
- //
- boolean also = false;
- if (!Utils.isEmpty(selectedNotes) && previousNoteLocations != null) {
- int[] indexes = pipelineMeta.getNoteIndexes(selectedNotes);
-
- also = !Utils.isEmpty(selectedTransforms);
- hopGui.undoDelegate.addUndoPosition(
- pipelineMeta,
- selectedNotes.toArray(new NotePadMeta[0]),
- indexes,
- previousNoteLocations,
- pipelineMeta.getSelectedNoteLocations(),
- also);
- }
- if (selectedTransforms != null
- && !selectedTransforms.isEmpty()
- && previousTransformLocations != null) {
- int[] indexes =
pipelineMeta.getTransformIndexes(selectedTransforms);
- hopGui.undoDelegate.addUndoPosition(
- pipelineMeta,
- selectedTransforms.toArray(new TransformMeta[0]),
- indexes,
- previousTransformLocations,
- pipelineMeta.getSelectedTransformLocations(),
- also);
- }
+ // Position undo was recorded at drag start via
markPositionUndoPoint().
}
}
@@ -1366,6 +1359,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
dragSelection = false;
iconDragStartScreen = null;
iconDragCommitted = false;
+ resetPositionUndoMark();
removePlacementDragFilters();
updateGui();
@@ -1406,34 +1400,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
pipelineGridDelegate.onPipelineSelectionChanged();
}
- // We moved around some items: store undo info...
-
- boolean also = false;
- if (selectedNotes != null
- && !selectedNotes.isEmpty()
- && previousNoteLocations != null) {
- int[] indexes = pipelineMeta.getNoteIndexes(selectedNotes);
- hopGui.undoDelegate.addUndoPosition(
- pipelineMeta,
- selectedNotes.toArray(new NotePadMeta[selectedNotes.size()]),
- indexes,
- previousNoteLocations,
- pipelineMeta.getSelectedNoteLocations(),
- also);
- also = !Utils.isEmpty(selectedTransforms);
- }
- if (selectedTransforms != null
- && !selectedTransforms.isEmpty()
- && previousTransformLocations != null) {
- int[] indexes =
pipelineMeta.getTransformIndexes(selectedTransforms);
- hopGui.undoDelegate.addUndoPosition(
- pipelineMeta,
- selectedTransforms.toArray(new
TransformMeta[selectedTransforms.size()]),
- indexes,
- previousTransformLocations,
- pipelineMeta.getSelectedTransformLocations(),
- also);
- }
+ // Position undo was recorded at drag start via
markPositionUndoPoint().
}
}
@@ -2239,6 +2206,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
int thresholdSq = ICON_DRAG_THRESHOLD_PX * ICON_DRAG_THRESHOLD_PX;
if (dx * dx + dy * dy > thresholdSq) {
iconDragCommitted = true;
+ markPositionUndoPoint();
canvas.setData("mode", "drag");
dragSelection = true;
selectedTransforms = pipelineMeta.getSelectedTransforms();
@@ -2368,7 +2336,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
showToolTip(new org.eclipse.swt.graphics.Point(event.x,
event.y));
}
}
- } else if (endHopTransform != null) {
+ } else {
if (ioMeta.isOutputProducer()) {
candidate = new PipelineHopMeta(transformMeta, endHopTransform);
endHopLocation = null;
@@ -2396,6 +2364,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
*
* new : new position of the note (not the mouse pointer) dx :
difference with previous position
*/
+ markPositionUndoPoint();
int dx = note.x - selectedNote.getLocation().x;
int dy = note.y - selectedNote.getLocation().y;
@@ -2498,6 +2467,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
Point[] transformsBefore = captureLocations(transforms);
Point[] notesBefore = captureNoteLocations(notes);
+ byte[] beforeSnapshot = captureUndoSnapshot();
moveSelected(dx, dy);
@@ -2509,28 +2479,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
return true;
}
- // Record notes first, then transforms, linked into a single undo action
(nextAlso).
- boolean also = false;
- if (!Utils.isEmpty(notes)) {
- also = !Utils.isEmpty(transforms);
- hopGui.undoDelegate.addUndoPosition(
- pipelineMeta,
- notes.toArray(new NotePadMeta[0]),
- pipelineMeta.getNoteIndexes(notes),
- notesBefore,
- notesAfter,
- also);
- }
- if (!Utils.isEmpty(transforms)) {
- hopGui.undoDelegate.addUndoPosition(
- pipelineMeta,
- transforms.toArray(new TransformMeta[0]),
- pipelineMeta.getTransformIndexes(transforms),
- transformsBefore,
- transformsAfter,
- also);
- }
-
+ commitDialogUndo(beforeSnapshot);
pipelineMeta.setChanged();
updateGui();
return true;
@@ -2663,6 +2612,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
Display disp = hopDisplay();
SwtUniversalImage swtImage =
SwtGc.getNativeImage(BasePainter.getStreamIconImage(stream.getStreamIcon(),
true));
+ assert swtImage != null;
return swtImage.getAsBitmapForSize(disp, ConstUi.SMALL_ICON_SIZE,
ConstUi.SMALL_ICON_SIZE);
}
@@ -2849,48 +2799,18 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
selectionRegion = null;
List<TransformMeta> subset = null;
- List<TransformMeta> moving;
if (selectionOnly) {
subset = pipelineMeta.getSelectedTransforms();
if (subset == null || subset.size() < 2) {
return; // Nothing meaningful to arrange.
}
- moving = new ArrayList<>(subset);
- } else {
- int n = pipelineMeta.nrTransforms();
- if (n == 0) {
- return;
- }
- moving = new ArrayList<>(n);
- for (int i = 0; i < n; i++) {
- moving.add(pipelineMeta.getTransform(i));
- }
+ } else if (pipelineMeta.nrTransforms() == 0) {
+ return;
}
- // Auto-layout may also reposition notes; capture them so the whole thing
is one undo step.
- List<NotePadMeta> notes = new ArrayList<>(pipelineMeta.getNotes());
- Point[] notesBefore = captureNoteLocations(notes);
-
- Point[] before = captureLocations(moving);
+ byte[] beforeSnapshot = captureUndoSnapshot();
PipelineMetaLayout.layout(pipelineMeta,
PropsUi.getInstance().getAutoLayoutOptions(), subset);
- Point[] after = captureLocations(moving);
- Point[] notesAfter = captureNoteLocations(notes);
-
- // Record notes first, then transforms, linked into a single undo action
(nextAlso).
- boolean also = false;
- if (!notes.isEmpty()) {
- also = true;
- hopGui.undoDelegate.addUndoPosition(
- pipelineMeta,
- notes.toArray(new NotePadMeta[0]),
- pipelineMeta.getNoteIndexes(notes),
- notesBefore,
- notesAfter,
- also);
- }
- int[] indexes = pipelineMeta.getTransformIndexes(moving);
- hopGui.undoDelegate.addUndoPosition(
- pipelineMeta, moving.toArray(new TransformMeta[0]), indexes, before,
after, also);
+ commitDialogUndo(beforeSnapshot);
pipelineMeta.setChanged();
updateGui();
@@ -2948,10 +2868,6 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
return PaletteEngineFilter.getPipelineEngineLabels();
}
- /**
- * Push the persisted design-engine label into the toolbar combo so the user
sees their previous
- * choice on every new tab. Called from {@link #addToolBar} after the
widgets are created.
- */
/**
* Dispose the combo ToolItem and the preceding label-separator ToolItem the
toolbar framework
* inserts for any item whose {@code @GuiToolbarElement.label} is non-empty
(see {@code
@@ -3490,7 +3406,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
EnterSelectionDialog dialog =
new EnterSelectionDialog(
hopShell(),
- choices.toArray(new String[choices.size()]),
+ choices.toArray(new String[0]),
BaseMessages.getString(PKG,
"HopGuiPipelineGraph.DistributionMethodDialog.Header"),
BaseMessages.getString(PKG,
"HopGuiPipelineGraph.DistributionMethodDialog.Text"));
if (dialog.open() != null) {
@@ -3536,7 +3452,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
return true;
}
// Variable pattern: '${VARIABLE}'
- return trimmed.matches("\\$\\{[^}]+\\}");
+ return trimmed.matches("\\$\\{[^}]+}");
}
public void copies(TransformMeta transformMeta) {
@@ -3573,7 +3489,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
SWT.YES | SWT.ICON_WARNING);
}
String cps = transformMeta.getCopiesString();
- if ((cps != null && !cps.equals(cop)) || (cps == null && cop != null)) {
+ if (cps == null || !cps.equals(cop)) {
transformMeta.setChanged();
}
transformMeta.setCopiesString(cop);
@@ -3659,8 +3575,8 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
* We're filtering out the disable action for hops which are already
disabled. The same for the
* enabled hops.
*
- * @param contextActionId
- * @param context
+ * @param contextActionId The context action ID to verify
+ * @param context The context to use
* @return True if the action should be shown and false otherwise.
*/
@GuiContextActionFilter(parentId = HopGuiPipelineHopContext.CONTEXT_ID)
@@ -3678,8 +3594,8 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
/**
* We're filtering out certain actions for transforms which don't make sense.
*
- * @param contextActionId
- * @param context
+ * @param contextActionId The context action ID to verify
+ * @param context The context
* @return True if the action should be shown and false otherwise.
*/
@GuiContextActionFilter(parentId = HopGuiPipelineTransformContext.CONTEXT_ID)
@@ -4073,7 +3989,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
categoryOrder = "1")
public void copyNotePadToClipboard(HopGuiPipelineNoteContext context) {
pipelineClipboardDelegate.copySelected(
- pipelineMeta, Collections.emptyList(),
Arrays.asList(context.getNotePadMeta()));
+ pipelineMeta, Collections.emptyList(),
Collections.singletonList(context.getNotePadMeta()));
}
@GuiContextAction(
@@ -4169,6 +4085,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
if (!Utils.isEmpty(targetTransforms[t])
&&
targetTransforms[t].equalsIgnoreCase(transformMeta.getName())) {
enabled = false;
+ break;
}
}
}
@@ -4176,11 +4093,11 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
return enabled;
}
- private AreaOwner setToolTip(int x, int y, int screenX, int screenY) {
+ private void setToolTip(int x, int y, int screenX, int screenY) {
AreaOwner subject = null;
if (!hopGui.getProps().showToolTips() || openedContextDialog) {
- return subject;
+ return;
}
canvas.setToolTipText(null);
@@ -4204,17 +4121,20 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
}
break;
case TRANSFORM_PARTITIONING:
- TransformMeta transform = (TransformMeta) areaOwner.getParent();
- tip.append("Transform partitioning:")
- .append(Const.CR)
- .append("-----------------------")
- .append(Const.CR);
-
tip.append(transform.getTransformPartitioningMeta().toString()).append(Const.CR);
- if (transform.getTargetTransformPartitioningMeta() != null) {
- tip.append(Const.CR)
+ {
+ TransformMeta transform = (TransformMeta) areaOwner.getParent();
+ tip.append("Transform partitioning:")
.append(Const.CR)
- .append("TARGET: " +
transform.getTargetTransformPartitioningMeta().toString())
+ .append("-----------------------")
.append(Const.CR);
+
tip.append(transform.getTransformPartitioningMeta().toString()).append(Const.CR);
+ if (transform.getTargetTransformPartitioningMeta() != null) {
+ tip.append(Const.CR)
+ .append(Const.CR)
+ .append("TARGET: ")
+
.append(transform.getTargetTransformPartitioningMeta().toString())
+ .append(Const.CR);
+ }
}
break;
case TRANSFORM_FAILURE_ICON:
@@ -4223,73 +4143,84 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
tipImage = GuiResource.getInstance().getImageFailure();
break;
case HOP_COPY_ICON:
- transform = (TransformMeta) areaOwner.getParent();
- tip.append(
- BaseMessages.getString(
- PKG, "PipelineGraph.Hop.Tooltip.HopTypeCopy",
transform.getName(), Const.CR));
- tipImage = GuiResource.getInstance().getImageCopyHop();
+ {
+ TransformMeta transform = (TransformMeta) areaOwner.getParent();
+ tip.append(
+ BaseMessages.getString(
+ PKG, "PipelineGraph.Hop.Tooltip.HopTypeCopy",
transform.getName(), Const.CR));
+ tipImage = GuiResource.getInstance().getImageCopyHop();
+ }
break;
case ROW_DISTRIBUTION_ICON:
- transform = (TransformMeta) areaOwner.getParent();
- tip.append(
- BaseMessages.getString(
- PKG,
- "PipelineGraph.Hop.Tooltip.RowDistribution",
- transform.getName(),
- transform.getRowDistribution() == null
- ? ""
- : transform.getRowDistribution().getDescription()));
- tip.append(Const.CR);
- tipImage = GuiResource.getInstance().getImageBalance();
+ {
+ TransformMeta transform = (TransformMeta) areaOwner.getParent();
+ tip.append(
+ BaseMessages.getString(
+ PKG,
+ "PipelineGraph.Hop.Tooltip.RowDistribution",
+ transform.getName(),
+ transform.getRowDistribution() == null
+ ? ""
+ : transform.getRowDistribution().getDescription()));
+ tip.append(Const.CR);
+ tipImage = GuiResource.getInstance().getImageBalance();
+ }
break;
case HOP_INFO_ICON:
- TransformMeta from = (TransformMeta) areaOwner.getParent();
- TransformMeta to = (TransformMeta) areaOwner.getOwner();
- tip.append(
- BaseMessages.getString(
- PKG,
- "PipelineGraph.Hop.Tooltip.HopTypeInfo",
- to.getName(),
- from.getName(),
- Const.CR));
- tipImage = GuiResource.getInstance().getImageInfo();
+ {
+ TransformMeta from = (TransformMeta) areaOwner.getParent();
+ TransformMeta to = (TransformMeta) areaOwner.getOwner();
+ tip.append(
+ BaseMessages.getString(
+ PKG,
+ "PipelineGraph.Hop.Tooltip.HopTypeInfo",
+ to.getName(),
+ from.getName(),
+ Const.CR));
+ tipImage = GuiResource.getInstance().getImageInfo();
+ }
break;
case HOP_ERROR_ICON:
- from = (TransformMeta) areaOwner.getParent();
- to = (TransformMeta) areaOwner.getOwner();
- areaOwner.getOwner();
- tip.append(
- BaseMessages.getString(
- PKG,
- "PipelineGraph.Hop.Tooltip.HopTypeError",
- from.getName(),
- to.getName(),
- Const.CR));
- tipImage = GuiResource.getInstance().getImageError();
+ {
+ TransformMeta from = (TransformMeta) areaOwner.getParent();
+ TransformMeta to = (TransformMeta) areaOwner.getOwner();
+ tip.append(
+ BaseMessages.getString(
+ PKG,
+ "PipelineGraph.Hop.Tooltip.HopTypeError",
+ from.getName(),
+ to.getName(),
+ Const.CR));
+ tipImage = GuiResource.getInstance().getImageError();
+ }
break;
case HOP_INFO_TRANSFORM_COPIES_ERROR:
- from = (TransformMeta) areaOwner.getParent();
- to = (TransformMeta) areaOwner.getOwner();
- tip.append(
- BaseMessages.getString(
- PKG,
- "PipelineGraph.Hop.Tooltip.InfoTransformCopies",
- from.getName(),
- to.getName(),
- Const.CR));
- tipImage = GuiResource.getInstance().getImageError();
+ {
+ TransformMeta from = (TransformMeta) areaOwner.getParent();
+ TransformMeta to = (TransformMeta) areaOwner.getOwner();
+ tip.append(
+ BaseMessages.getString(
+ PKG,
+ "PipelineGraph.Hop.Tooltip.InfoTransformCopies",
+ from.getName(),
+ to.getName(),
+ Const.CR));
+ tipImage = GuiResource.getInstance().getImageError();
+ }
break;
case HOP_INFO_TRANSFORMS_PARTITIONED:
- from = (TransformMeta) areaOwner.getParent();
- to = (TransformMeta) areaOwner.getOwner();
- tip.append(
- BaseMessages.getString(
- PKG,
- "PipelineGraph.Hop.Tooltip.InfoTransformsPartitioned",
- from.getName(),
- to.getName(),
- Const.CR));
- tipImage = GuiResource.getInstance().getImageError();
+ {
+ TransformMeta from = (TransformMeta) areaOwner.getParent();
+ TransformMeta to = (TransformMeta) areaOwner.getOwner();
+ tip.append(
+ BaseMessages.getString(
+ PKG,
+ "PipelineGraph.Hop.Tooltip.InfoTransformsPartitioned",
+ from.getName(),
+ to.getName(),
+ Const.CR));
+ tipImage = GuiResource.getInstance().getImageError();
+ }
break;
case TRANSFORM_TARGET_HOP_ICON:
@@ -4319,9 +4250,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
"PipelineGraph.DeprecatedTransform.Tooltip.Message1",
iconTransformMeta.getName());
int length = tipNext.length() + 5;
- for (int i = 0; i < length; i++) {
- tip.append("-");
- }
+ tip.repeat("-", Math.max(0, length));
tip.append(Const.CR).append(tipNext).append(Const.CR);
tip.append(
BaseMessages.getString(PKG,
"PipelineGraph.DeprecatedTransform.Tooltip.Message2"));
@@ -4343,7 +4272,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
case TRANSFORM_OUTPUT_DATA:
RowBuffer rowBuffer = (RowBuffer) areaOwner.getOwner();
if (rowBuffer != null && !rowBuffer.isEmpty()) {
- tip.append("Available output rows: " + rowBuffer.size());
+ tip.append("Available output rows: ").append(rowBuffer.size());
tipImage = GuiResource.getInstance().getImageData();
}
break;
@@ -4359,7 +4288,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
.append(hopMeta.getToTransform().getName())
.append(Const.CR);
}
- tip.append("Available output rows: " + hopRowBuffer.size());
+ tip.append("Available output rows: ").append(hopRowBuffer.size());
tipImage = GuiResource.getInstance().getImageData();
}
break;
@@ -4435,8 +4364,6 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
}
showSpecialTooltip(newTip, screenX, screenY);
}
-
- return subject;
}
public void showSpecialTooltip(String label, int screenX, int screenY) {
@@ -4532,7 +4459,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
while (pmd.getShell() == null
|| (!pmd.getShell().isDisposed() && !monitor.isCanceled())) {
try {
- Thread.sleep(250);
+ sleep(250);
} catch (InterruptedException e) {
// Ignore
}
@@ -4890,7 +4817,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
List<TransformMeta> selection = pipelineMeta.getSelectedTransforms();
int[] indices = pipelineMeta.getTransformIndexes(selection);
- return new SnapAllignDistribute(pipelineMeta, selection, indices,
hopGui.undoDelegate, this);
+ return new SnapAllignDistribute(pipelineMeta, selection, indices, null,
this);
}
@GuiToolbarElement(
@@ -4998,14 +4925,14 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
return pipelineMeta.hasChanged();
}
- public boolean editProperties(PipelineMeta pipelineMeta, HopGui hopGui) {
- return editProperties(pipelineMeta, hopGui, null);
+ public void editProperties(PipelineMeta pipelineMeta, HopGui hopGui) {
+ editProperties(pipelineMeta, hopGui, null);
}
- public boolean editProperties(
+ public void editProperties(
PipelineMeta pipelineMeta, HopGui hopGui, PipelineDialog.Tabs
currentTab) {
if (pipelineMeta == null) {
- return false;
+ return;
}
Shell shell = hopGui.getActiveShell();
@@ -5016,9 +4943,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
if (tid.open() != null) {
hopGui.setParametersAsVariablesInUI(pipelineMeta, variables);
updateGui();
- return true;
}
- return false;
}
@Override
@@ -5054,6 +4979,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
out.write(XmlHandler.getXmlHeader(Const.UTF_8).getBytes(StandardCharsets.UTF_8));
out.write(xml.getBytes(StandardCharsets.UTF_8));
pipelineMeta.clearChanged();
+ rememberSavedSnapshot();
updateGui();
} finally {
out.flush();
@@ -5564,8 +5490,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
if (extraViewTabFolder != null
&& !extraViewTabFolder.isDisposed()
&& extraViewTabFolder.getItemCount() > 0) {
- extraViewTabFolder.setSelection(
- Math.max(0, Math.min(index, extraViewTabFolder.getItemCount() - 1)));
+ extraViewTabFolder.setSelection(Math.clamp(index, 0,
extraViewTabFolder.getItemCount() - 1));
}
}
@@ -6275,11 +6200,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
messageBox.open();
return;
}
- if (pipeline.isFinished()) {
- // Show collected sample data...
- //
-
- } else {
+ if (!pipeline.isFinished()) {
try {
pipeline.retrieveComponentOutput(
hopGui.getVariables(),
@@ -6314,8 +6235,8 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
/**
* Edit the transform of the given pipeline
*
- * @param pipelineMeta
- * @param transformMeta
+ * @param pipelineMeta The pipeline metadata to reference
+ * @param transformMeta The transform metadata to edit
*/
public void editTransform(PipelineMeta pipelineMeta, TransformMeta
transformMeta) {
pipelineTransformDelegate.editTransform(pipelineMeta, transformMeta);
@@ -6344,9 +6265,9 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
*
* <p>Prompt auto save feature...
*
- * @param pipelineMeta
+ * @param pipelineMeta The pipeline to handle changes for
* @return true if pipeline meta has name and if changed is saved
- * @throws HopException
+ * @throws HopException In case something goes wrong
*/
public boolean handlePipelineMetaChanges(PipelineMeta pipelineMeta) throws
HopException {
if (pipelineMeta.hasChanged()) {
@@ -6508,6 +6429,69 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
return Objects.hash(pipelineMeta, id);
}
+ @Override
+ public boolean isUndoMeta(IUndo undoInterface) {
+ return undoInterface == pipelineMeta;
+ }
+
+ @Override
+ public void markUndoPoint() {
+ snapshotUndo.markUndoPoint();
+ }
+
+ @Override
+ public byte[] captureUndoSnapshot() {
+ return snapshotUndo.captureUndoSnapshot();
+ }
+
+ @Override
+ public void commitDialogUndo(byte[] before) {
+ snapshotUndo.commitDialogUndo(before);
+ }
+
+ @Override
+ public void recordAfterChange(boolean nextAlso) {
+ snapshotUndo.recordAfterChange(nextAlso);
+ }
+
+ @Override
+ public void markPositionUndoPoint() {
+ snapshotUndo.markPositionUndoPoint();
+ }
+
+ @Override
+ public void resetPositionUndoMark() {
+ snapshotUndo.resetPositionUndoMark();
+ }
+
+ @Override
+ public void rememberSavedSnapshot() {
+ snapshotUndo.rememberSavedSnapshot();
+ }
+
+ @Override
+ public boolean canUndo() {
+ return snapshotUndo.canUndo();
+ }
+
+ @Override
+ public boolean canRedo() {
+ return snapshotUndo.canRedo();
+ }
+
+ private void restoreAfterSnapshot() {
+ if (pipelineMeta != null) {
+ pipelineMeta.setInternalHopVariables(variables);
+ }
+ clearSettings();
+ snapshotUndo.resetPositionUndoMark();
+ if (pipelineGridDelegate != null) {
+ pipelineGridDelegate.onPipelineSelectionChanged();
+ }
+ updateGui();
+ redraw();
+ }
+
@GuiToolbarElement(
root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
id = TOOLBAR_ITEM_UNDO_ID,
@@ -6519,7 +6503,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
@GuiOsxKeyboardShortcut(command = true, key = 'z')
@Override
public void undo() {
- pipelineUndoDelegate.undoPipelineAction(this, pipelineMeta);
+ snapshotUndo.undo();
forceFocus();
}
@@ -6533,7 +6517,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
@GuiOsxKeyboardShortcut(command = true, shift = true, key = 'z')
@Override
public void redo() {
- pipelineUndoDelegate.redoPipelineAction(this, pipelineMeta);
+ snapshotUndo.redo();
forceFocus();
}
@@ -6554,10 +6538,9 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
// Enable/disable the undo/redo toolbar buttons...
//
- toolBarWidgets.enableToolbarItem(
- TOOLBAR_ITEM_UNDO_ID, pipelineMeta.viewThisUndo() != null);
- toolBarWidgets.enableToolbarItem(
- TOOLBAR_ITEM_REDO_ID, pipelineMeta.viewNextUndo() != null);
+ snapshotUndo.refreshLastSnapshot();
+ toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_UNDO_ID,
snapshotUndo.canUndo());
+ toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_REDO_ID,
snapshotUndo.canRedo());
// Enable/disable the execution toolbar buttons
//
@@ -6581,7 +6564,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
toolBarWidgets.enableToolbarItem(
TOOLBAR_ITEM_TO_EXECUTION_INFO, hasExecutionInfoLocations);
- hopGui.setUndoMenu(pipelineMeta);
+ hopGui.setUndoMenu(snapshotUndo.canUndo(),
snapshotUndo.canRedo());
hopGui.handleFileCapabilities(fileType,
pipelineMeta.hasChanged(), running, paused);
// Enable the align/distribute menus if one or more transforms
are selected.
@@ -6701,7 +6684,9 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
categoryOrder = "1")
public void copyTransformToClipboard(HopGuiPipelineTransformContext context)
{
pipelineClipboardDelegate.copySelected(
- pipelineMeta, Arrays.asList(context.getTransformMeta()),
Collections.emptyList());
+ pipelineMeta,
+ Collections.singletonList(context.getTransformMeta()),
+ Collections.emptyList());
}
@GuiKeyboardShortcut(key = ' ')
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineClipboardDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineClipboardDelegate.java
index 8cff22ab73..ea9e97f205 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineClipboardDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineClipboardDelegate.java
@@ -116,8 +116,7 @@ public class HopGuiPipelineClipboardDelegate {
offset = new Point(-min.x, -min.y);
}
- // Undo/redo object positions...
- int[] position = new int[transforms.size()];
+ pipelineGraph.markUndoPoint();
for (int i = 0; i < transforms.size(); i++) {
Point p = transforms.get(i).getLocation();
@@ -129,7 +128,6 @@ public class HopGuiPipelineClipboardDelegate {
transformOldNames.add(name);
transforms.get(i).setName(pipelineMeta.getAlternativeTransformName(name));
pipelineMeta.addTransform(transforms.get(i));
- position[i] = pipelineMeta.indexOfTransform(transforms.get(i));
transforms.get(i).setSelected(true);
}
@@ -184,23 +182,7 @@ public class HopGuiPipelineClipboardDelegate {
}
}
- // Save undo information too...
- hopGui.undoDelegate.addUndoNew(
- pipelineMeta, transforms.toArray(new TransformMeta[0]), position,
false);
-
- int[] hopPos = new int[hops.size()];
- for (int i = 0; i < hops.size(); i++) {
- hopPos[i] = pipelineMeta.indexOfPipelineHop(hops.get(i));
- }
- hopGui.undoDelegate.addUndoNew(
- pipelineMeta, hops.toArray(new PipelineHopMeta[0]), hopPos, true);
-
- int[] notePos = new int[notes.size()];
- for (int i = 0; i < notes.size(); i++) {
- notePos[i] = pipelineMeta.indexOfNote(notes.get(i));
- }
- hopGui.undoDelegate.addUndoNew(
- pipelineMeta, notes.toArray(new NotePadMeta[0]), notePos, true);
+ // Undo was recorded once before adding the pasted objects.
} catch (HopException e) {
// See if this was different (non-XML) content
//
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineTransformDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineTransformDelegate.java
index f17055e354..34cfb87777 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineTransformDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineTransformDelegate.java
@@ -199,6 +199,7 @@ public class HopGuiPipelineTransformDelegate {
dialog = getTransformDialog(transformMeta.getTransform(), pipelineMeta,
name);
TransformMeta before = null;
+ byte[] beforeSnapshot = null;
if (dialog != null) {
dialogs.put(name, dialog);
@@ -206,6 +207,7 @@ public class HopGuiPipelineTransformDelegate {
transformMeta.getTransform().convertIOMetaToTransformNames();
// Snapshot after IO-meta normalization so OK-without-edits is not
treated as a change.
before = (TransformMeta) transformMeta.clone();
+ beforeSnapshot = pipelineGraph.captureUndoSnapshot();
// Subject stack covers legacy dialogs that never set
BaseDialog.DIALOG_SUBJECT
transformName =
BaseDialog.withDialogSubject(transformMeta.getTransform(), dialog::open);
@@ -261,13 +263,9 @@ public class HopGuiPipelineTransformDelegate {
transformMeta.setName(transformName);
TransformMeta after = (TransformMeta) transformMeta.clone();
+ pipelineGraph.commitDialogUndo(beforeSnapshot);
if (hasTransformMetaChanged(before, after)) {
transformMeta.setChanged();
- hopGui.undoDelegate.addUndoChange(
- pipelineMeta,
- new TransformMeta[] {before},
- new TransformMeta[] {after},
- new int[] {pipelineMeta.indexOfTransform(transformMeta)});
} else {
transformMeta.setChanged(before.hasChanged());
}
@@ -549,6 +547,7 @@ public class HopGuiPipelineTransformDelegate {
}
public void editTransformPartitioning(PipelineMeta pipelineMeta,
TransformMeta transformMeta) {
+ byte[] beforeSnapshot = pipelineGraph.captureUndoSnapshot();
String[] schemaNames;
try {
schemaNames = hopGui.partitionManager.getNamesArray();
@@ -607,17 +606,9 @@ public class HopGuiPipelineTransformDelegate {
TransformMeta partitionBefore = partitionSettings.getBefore();
TransformMeta partitionAfter = partitionSettings.getAfter();
+ pipelineGraph.commitDialogUndo(beforeSnapshot);
if (hasTransformMetaChanged(partitionBefore, partitionAfter)) {
transformMeta.setChanged();
- hopGui.undoDelegate.addUndoChange(
- partitionSettings.getPipelineMeta(),
- new TransformMeta[] {partitionBefore},
- new TransformMeta[] {partitionAfter},
- new int[] {
- partitionSettings
- .getPipelineMeta()
- .indexOfTransform(partitionSettings.getTransformMeta())
- });
} else {
transformMeta.setChanged(partitionBefore.hasChanged());
}
@@ -686,6 +677,7 @@ public class HopGuiPipelineTransformDelegate {
// now edit this transformErrorMeta object:
TransformMeta before = (TransformMeta) transformMeta.clone();
+ byte[] beforeSnapshot = pipelineGraph.captureUndoSnapshot();
TransformErrorMetaDialog dialog =
new TransformErrorMetaDialog(
hopGui.getActiveShell(),
@@ -697,6 +689,7 @@ public class HopGuiPipelineTransformDelegate {
BaseDialog.withDialogSubject(transformMeta.getTransform(),
dialog::open))) {
transformMeta.setTransformErrorMeta(transformErrorMeta);
TransformMeta after = (TransformMeta) transformMeta.clone();
+ pipelineGraph.commitDialogUndo(beforeSnapshot);
if (hasTransformMetaChanged(before, after)) {
transformMeta.setChanged();
} else {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineUndoDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineUndoDelegate.java
index 8876ff0d3e..48a71e060c 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineUndoDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineUndoDelegate.java
@@ -17,339 +17,45 @@
package org.apache.hop.ui.hopgui.file.pipeline.delegates;
-import org.apache.hop.core.NotePadMeta;
-import org.apache.hop.core.gui.Point;
-import org.apache.hop.core.undo.ChangeAction;
-import org.apache.hop.pipeline.PipelineHopMeta;
import org.apache.hop.pipeline.PipelineMeta;
-import org.apache.hop.pipeline.transform.TransformMeta;
import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
import org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
+/**
+ * Pipeline undo/redo is implemented with gzip XML snapshots on {@link
HopGuiPipelineGraph}. This
+ * class remains as a hook for any leftover callers.
+ */
public class HopGuiPipelineUndoDelegate {
private HopGuiPipelineGraph pipelineGraph;
private HopGui hopGui;
- /**
- * @param hopGui
- */
public HopGuiPipelineUndoDelegate(HopGui hopGui, HopGuiPipelineGraph
pipelineGraph) {
this.hopGui = hopGui;
this.pipelineGraph = pipelineGraph;
}
public void undoPipelineAction(IHopFileTypeHandler handler, PipelineMeta
pipelineMeta) {
- ChangeAction changeAction = pipelineMeta.previousUndo();
- if (changeAction == null) {
- return;
- }
- undoPipelineAction(handler, pipelineMeta, changeAction);
- handler.updateGui();
- }
-
- public void undoPipelineAction(
- IHopFileTypeHandler handler, PipelineMeta pipelineMeta, ChangeAction
changeAction) {
- switch (changeAction.getType()) {
- // We created a new transform : undo this...
- case NewTransform:
- // Delete the transform at correct location:
- for (int i = changeAction.getCurrent().length - 1; i >= 0; i--) {
- int idx = changeAction.getCurrentIndex()[i];
- pipelineMeta.removeTransform(idx);
- }
- break;
-
- // We created a new note : undo this...
- case NewNote:
- // Delete the note at correct location:
- for (int i = changeAction.getCurrent().length - 1; i >= 0; i--) {
- int idx = changeAction.getCurrentIndex()[i];
- pipelineMeta.removeNote(idx);
- }
- break;
-
- // We created a new hop : undo this...
- case NewPipelineHop:
- // Delete the hop at correct location:
- for (int i = changeAction.getCurrent().length - 1; i >= 0; i--) {
- int idx = changeAction.getCurrentIndex()[i];
- pipelineMeta.removePipelineHop(idx);
- }
- break;
-
- //
- // DELETE
- //
-
- // We delete a transform : undo this...
- case DeleteTransform:
- // un-Delete the transform at correct location: re-insert
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- TransformMeta transformMeta = (TransformMeta)
changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
- pipelineMeta.addTransform(idx, transformMeta);
- }
- break;
-
- // We delete new note : undo this...
- case DeleteNote:
- // re-insert the note at correct location:
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- NotePadMeta ni = (NotePadMeta) changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
- pipelineMeta.addNote(idx, ni);
- }
- break;
-
- // We deleted a hop : undo this...
- case DeletePipelineHop:
- // re-insert the hop at correct location:
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- PipelineHopMeta hi = (PipelineHopMeta) changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
- // Build a new hop:
- TransformMeta from =
pipelineMeta.findTransform(hi.getFromTransform().getName());
- TransformMeta to =
pipelineMeta.findTransform(hi.getToTransform().getName());
- PipelineHopMeta hinew = new PipelineHopMeta(from, to);
- pipelineMeta.addPipelineHop(idx, hinew);
- }
- break;
-
- //
- // CHANGE
- //
-
- // We changed a transform : undo this...
- case ChangeTransform:
- // Delete the current transform, insert previous version.
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- TransformMeta prev =
- (TransformMeta) ((TransformMeta)
changeAction.getPrevious()[i]).clone();
- int idx = changeAction.getCurrentIndex()[i];
-
- pipelineMeta.getTransform(idx).replaceMeta(prev);
- }
- break;
-
- // We changed a note : undo this...
- case ChangeNote:
- // Delete & re-insert
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- int idx = changeAction.getCurrentIndex()[i];
- pipelineMeta.removeNote(idx);
- NotePadMeta prev = (NotePadMeta) changeAction.getPrevious()[i];
- pipelineMeta.addNote(idx, (NotePadMeta) prev.clone());
- }
- break;
-
- // We changed a hop : undo this...
- case ChangePipelineHop:
- // Delete & re-insert
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- PipelineHopMeta prev = (PipelineHopMeta)
changeAction.getPrevious()[i];
- int idx = changeAction.getCurrentIndex()[i];
-
- pipelineMeta.removePipelineHop(idx);
- pipelineMeta.addPipelineHop(idx, (PipelineHopMeta) prev.clone());
- }
- break;
-
- //
- // POSITION
- //
-
- // The position of a transform has changed: undo this...
- case PositionTransform:
- // Find the location of the transform:
- for (int i = 0; i < changeAction.getCurrentIndex().length; i++) {
- TransformMeta transformMeta =
- pipelineMeta.getTransform(changeAction.getCurrentIndex()[i]);
- transformMeta.setLocation(changeAction.getPreviousLocation()[i]);
- }
- break;
-
- // The position of a note has changed: undo this...
- case PositionNote:
- for (int i = 0; i < changeAction.getCurrentIndex().length; i++) {
- int idx = changeAction.getCurrentIndex()[i];
- NotePadMeta npi = pipelineMeta.getNote(idx);
- Point prev = changeAction.getPreviousLocation()[i];
- npi.setLocation(prev);
- }
- break;
- default:
- break;
- }
-
- // OK, now check if we need to do this again...
- if (pipelineMeta.viewNextUndo() != null &&
pipelineMeta.viewNextUndo().getNextAlso()) {
- undoPipelineAction(handler, pipelineMeta);
- }
+ pipelineGraph.undo();
}
public void redoPipelineAction(IHopFileTypeHandler handler, PipelineMeta
pipelineMeta) {
- ChangeAction changeAction = pipelineMeta.nextUndo();
- if (changeAction == null) {
- return;
- }
- redoPipelineAction(handler, pipelineMeta, changeAction);
- handler.updateGui();
- }
-
- public void redoPipelineAction(
- IHopFileTypeHandler handler, PipelineMeta pipelineMeta, ChangeAction
changeAction) {
- switch (changeAction.getType()) {
- case NewTransform:
- // re-delete the transform at correct location:
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- TransformMeta transformMeta = (TransformMeta)
changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
- pipelineMeta.addTransform(idx, transformMeta);
- }
- break;
-
- case NewNote:
- // re-insert the note at correct location:
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- NotePadMeta ni = (NotePadMeta) changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
- pipelineMeta.addNote(idx, ni);
- }
- break;
-
- case NewPipelineHop:
- // re-insert the hop at correct location:
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- PipelineHopMeta hi = (PipelineHopMeta) changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
- pipelineMeta.addPipelineHop(idx, hi);
- }
- break;
-
- //
- // DELETE
- //
- case DeleteTransform:
- // re-remove the transform at correct location:
- for (int i = changeAction.getCurrent().length - 1; i >= 0; i--) {
- int idx = changeAction.getCurrentIndex()[i];
- pipelineMeta.removeTransform(idx);
- }
- break;
-
- case DeleteNote:
- // re-remove the note at correct location:
- for (int i = changeAction.getCurrent().length - 1; i >= 0; i--) {
- int idx = changeAction.getCurrentIndex()[i];
- pipelineMeta.removeNote(idx);
- }
- break;
-
- case DeletePipelineHop:
- // re-remove the hop at correct location:
- for (int i = changeAction.getCurrent().length - 1; i >= 0; i--) {
- int idx = changeAction.getCurrentIndex()[i];
- pipelineMeta.removePipelineHop(idx);
- }
- break;
-
- //
- // CHANGE
- //
-
- // We changed a transform : undo this...
- case ChangeTransform:
- // Delete the current transform, insert previous version.
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- TransformMeta transformMeta =
- (TransformMeta) ((TransformMeta)
changeAction.getCurrent()[i]).clone();
-
pipelineMeta.getTransform(changeAction.getCurrentIndex()[i]).replaceMeta(transformMeta);
- }
- break;
-
- // We changed a note : undo this...
- case ChangeNote:
- // Delete & re-insert
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- NotePadMeta ni = (NotePadMeta) changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
-
- pipelineMeta.removeNote(idx);
- pipelineMeta.addNote(idx, (NotePadMeta) ni.clone());
- }
- break;
-
- // We changed a hop : undo this...
- case ChangePipelineHop:
- // Delete & re-insert
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- PipelineHopMeta hi = (PipelineHopMeta) changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
-
- pipelineMeta.removePipelineHop(idx);
- pipelineMeta.addPipelineHop(idx, (PipelineHopMeta) hi.clone());
- }
- break;
-
- //
- // CHANGE POSITION
- //
- case PositionTransform:
- for (int i = 0; i < changeAction.getCurrentIndex().length; i++) {
- // Find & change the location of the transform:
- TransformMeta transformMeta =
- pipelineMeta.getTransform(changeAction.getCurrentIndex()[i]);
- transformMeta.setLocation(changeAction.getCurrentLocation()[i]);
- }
- break;
- case PositionNote:
- for (int i = 0; i < changeAction.getCurrentIndex().length; i++) {
- int idx = changeAction.getCurrentIndex()[i];
- NotePadMeta npi = pipelineMeta.getNote(idx);
- Point curr = changeAction.getCurrentLocation()[i];
- npi.setLocation(curr);
- }
- break;
- default:
- break;
- }
-
- // OK, now check if we need to do this again...
- if (pipelineMeta.viewNextUndo() != null &&
pipelineMeta.viewNextUndo().getNextAlso()) {
- redoPipelineAction(handler, pipelineMeta);
- }
+ pipelineGraph.redo();
}
- /**
- * Gets pipelineGraph
- *
- * @return value of pipelineGraph
- */
public HopGuiPipelineGraph getPipelineGraph() {
return pipelineGraph;
}
- /**
- * @param pipelineGraph The pipelineGraph to set
- */
public void setPipelineGraph(HopGuiPipelineGraph pipelineGraph) {
this.pipelineGraph = pipelineGraph;
}
- /**
- * Gets hopGui
- *
- * @return value of hopGui
- */
public HopGui getHopGui() {
return hopGui;
}
- /**
- * @param hopGui The hopGui to set
- */
public void setHopGui(HopGui hopGui) {
this.hopGui = hopGui;
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/HopGuiAbstractGraph.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/HopGuiAbstractGraph.java
index a7ddd87a10..878fa7c348 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/HopGuiAbstractGraph.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/HopGuiAbstractGraph.java
@@ -179,32 +179,42 @@ public abstract class HopGuiAbstractGraph extends
DragViewZoomBase
public abstract SnapAllignDistribute createSnapAlignDistribute();
+ /** Record an undo snapshot before a structural change. Overridden by
pipeline/workflow graphs. */
+ protected void markUndoPoint() {
+ // no-op for graphs that do not implement snapshot undo
+ }
+
@Override
public void snapToGrid() {
snapToGrid(ConstUi.GRID_SIZE);
}
private void snapToGrid(int size) {
+ markUndoPoint();
createSnapAlignDistribute().snapToGrid(size);
setChanged();
}
public void alignLeft() {
+ markUndoPoint();
createSnapAlignDistribute().allignleft();
setChanged();
}
public void alignRight() {
+ markUndoPoint();
createSnapAlignDistribute().allignright();
setChanged();
}
public void alignTop() {
+ markUndoPoint();
createSnapAlignDistribute().alligntop();
setChanged();
}
public void alignBottom() {
+ markUndoPoint();
createSnapAlignDistribute().allignbottom();
setChanged();
}
@@ -212,12 +222,14 @@ public abstract class HopGuiAbstractGraph extends
DragViewZoomBase
@GuiKeyboardShortcut(alt = true, key = SWT.ARROW_RIGHT)
@GuiOsxKeyboardShortcut(alt = true, key = SWT.ARROW_RIGHT)
public void distributeHorizontal() {
+ markUndoPoint();
createSnapAlignDistribute().distributehorizontal();
setChanged();
}
@GuiOsxKeyboardShortcut(alt = true, key = SWT.ARROW_UP)
public void distributeVertical() {
+ markUndoPoint();
createSnapAlignDistribute().distributevertical();
setChanged();
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/HopGuiGraphSnapshotUndo.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/HopGuiGraphSnapshotUndo.java
new file mode 100644
index 0000000000..73dcdebf15
--- /dev/null
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/HopGuiGraphSnapshotUndo.java
@@ -0,0 +1,218 @@
+/*
+ * 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.hop.ui.hopgui.file.shared;
+
+import java.util.function.Supplier;
+import org.apache.hop.base.AbstractMeta;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.undo.XmlSnapshotUndo;
+import org.apache.hop.core.undo.XmlSnapshotUndo.ContentRestorer;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.hopgui.HopGui;
+
+/**
+ * Graph-owned gzip XML undo/redo stacks, ported from hop-data-vault {@code
ModelGraphSnapshotUndo}.
+ */
+public class HopGuiGraphSnapshotUndo<M extends AbstractMeta> {
+
+ private static final Class<?> PKG = HopGui.class;
+
+ private final HopGui hopGui;
+ private final XmlSnapshotUndo<M> engine;
+ private final Supplier<M> metaSupplier;
+ private final Supplier<String> filenameSupplier;
+ private final Runnable afterRestore;
+
+ private byte[] lastSnapshot;
+ private byte[] lastSavedSnapshot;
+ private boolean positionChangeUndoMarked;
+
+ public HopGuiGraphSnapshotUndo(
+ HopGui hopGui,
+ Class<M> type,
+ String xmlRootTag,
+ ContentRestorer<M> restorer,
+ Supplier<M> metaSupplier,
+ Supplier<String> filenameSupplier,
+ Runnable afterRestore) {
+ this.hopGui = hopGui;
+ this.engine =
+ new XmlSnapshotUndo<>(type, xmlRootTag, restorer, () ->
PropsUi.getInstance().getMaxUndo());
+ this.metaSupplier = metaSupplier;
+ this.filenameSupplier = filenameSupplier;
+ this.afterRestore = afterRestore;
+ }
+
+ public void initialize() {
+ lastSnapshot = captureQuiet();
+ lastSavedSnapshot = lastSnapshot;
+ }
+
+ public void rememberSavedSnapshot() {
+ lastSavedSnapshot = captureQuiet();
+ lastSnapshot = lastSavedSnapshot;
+ }
+
+ public void refreshLastSnapshot() {
+ if (engine.isApplyingSnapshot()) {
+ return;
+ }
+ lastSnapshot = captureQuiet();
+ }
+
+ public boolean canUndo() {
+ return engine.canUndo();
+ }
+
+ public boolean canRedo() {
+ return engine.canRedo();
+ }
+
+ public boolean isApplyingSnapshot() {
+ return engine.isApplyingSnapshot();
+ }
+
+ public void markUndoPoint() {
+ M model = metaSupplier.get();
+ if (model == null || engine.isApplyingSnapshot()) {
+ return;
+ }
+ try {
+ engine.markChange(model, metadataProvider());
+ } catch (HopException e) {
+ showRecordError(e);
+ }
+ }
+
+ public byte[] captureUndoSnapshot() {
+ return captureQuiet();
+ }
+
+ public void commitDialogUndo(byte[] beforeChange) {
+ if (beforeChange == null || engine.isApplyingSnapshot()) {
+ return;
+ }
+ byte[] after = captureQuiet();
+ if (after == null || XmlSnapshotUndo.sameXmlContent(beforeChange, after)) {
+ return;
+ }
+ engine.pushSnapshot(beforeChange);
+ lastSnapshot = after;
+ }
+
+ /**
+ * Compatibility hook for leftover {@code addUndo*} calls that fire after
the mutation. Pushes
+ * {@code lastSnapshot} (the pre-change document) unless {@code nextAlso}
indicates a chained
+ * follow-up of the same user action.
+ */
+ public void recordAfterChange(boolean nextAlso) {
+ if (engine.isApplyingSnapshot()) {
+ return;
+ }
+ if (!nextAlso && lastSnapshot != null) {
+ engine.pushSnapshot(lastSnapshot);
+ }
+ lastSnapshot = captureQuiet();
+ }
+
+ public void markPositionUndoPoint() {
+ if (!positionChangeUndoMarked) {
+ markUndoPoint();
+ positionChangeUndoMarked = true;
+ }
+ }
+
+ public void resetPositionUndoMark() {
+ positionChangeUndoMarked = false;
+ }
+
+ public void undo() {
+ apply(true);
+ }
+
+ public void redo() {
+ apply(false);
+ }
+
+ private void apply(boolean isUndo) {
+ M model = metaSupplier.get();
+ if (model == null) {
+ return;
+ }
+ try {
+ boolean applied =
+ isUndo
+ ? engine.undo(model, metadataProvider(), filenameSupplier.get())
+ : engine.redo(model, metadataProvider(), filenameSupplier.get());
+ if (!applied) {
+ return;
+ }
+ lastSnapshot = captureQuiet();
+ applyDirtyFlag(model);
+ if (afterRestore != null) {
+ afterRestore.run();
+ }
+ } catch (HopException e) {
+ showApplyError(e);
+ }
+ }
+
+ private void applyDirtyFlag(M model) {
+ if (lastSavedSnapshot != null
+ && XmlSnapshotUndo.sameXmlContent(lastSnapshot, lastSavedSnapshot)) {
+ model.clearChanged();
+ } else {
+ model.setChanged();
+ }
+ }
+
+ private byte[] captureQuiet() {
+ M model = metaSupplier.get();
+ if (model == null || engine.isApplyingSnapshot()) {
+ return null;
+ }
+ try {
+ return engine.captureSnapshot(model, metadataProvider());
+ } catch (HopException e) {
+ showRecordError(e);
+ return null;
+ }
+ }
+
+ private IHopMetadataProvider metadataProvider() {
+ return hopGui.getMetadataProvider();
+ }
+
+ private void showRecordError(Exception e) {
+ new ErrorDialog(
+ hopGui.getShell(),
+ BaseMessages.getString(PKG, "HopGui.Undo.Error.Record.Title"),
+ BaseMessages.getString(PKG, "HopGui.Undo.Error.Record.Message"),
+ e);
+ }
+
+ private void showApplyError(Exception e) {
+ new ErrorDialog(
+ hopGui.getShell(),
+ BaseMessages.getString(PKG, "HopGui.Undo.Error.Apply.Title"),
+ BaseMessages.getString(PKG, "HopGui.Undo.Error.Apply.Message"),
+ e);
+ }
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/ISnapshotUndoSupport.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/ISnapshotUndoSupport.java
new file mode 100644
index 0000000000..3d355616dd
--- /dev/null
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/shared/ISnapshotUndoSupport.java
@@ -0,0 +1,46 @@
+/*
+ * 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.hop.ui.hopgui.file.shared;
+
+import org.apache.hop.core.gui.IUndo;
+
+/**
+ * Snapshot-based undo/redo for pipeline and workflow graphs. {@code addUndo*}
call sites that still
+ * fire after a mutation are routed through {@link
#recordAfterChange(boolean)}.
+ */
+public interface ISnapshotUndoSupport {
+
+ boolean isUndoMeta(IUndo undoInterface);
+
+ void markUndoPoint();
+
+ byte[] captureUndoSnapshot();
+
+ void commitDialogUndo(byte[] before);
+
+ void recordAfterChange(boolean nextAlso);
+
+ void markPositionUndoPoint();
+
+ void resetPositionUndoMark();
+
+ void rememberSavedSnapshot();
+
+ boolean canUndo();
+
+ boolean canRedo();
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
index 5d96943c7b..8b26c9e414 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
@@ -36,7 +36,6 @@ import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.vfs2.FileName;
import org.apache.commons.vfs2.FileObject;
-import org.apache.hop.base.AbstractMeta;
import org.apache.hop.core.Const;
import org.apache.hop.core.IEngineMeta;
import org.apache.hop.core.NotePadMeta;
@@ -57,6 +56,7 @@ import org.apache.hop.core.gui.CanvasSvgRenderResult;
import org.apache.hop.core.gui.DPoint;
import org.apache.hop.core.gui.IGc;
import org.apache.hop.core.gui.IRedrawable;
+import org.apache.hop.core.gui.IUndo;
import org.apache.hop.core.gui.Point;
import org.apache.hop.core.gui.Rectangle;
import org.apache.hop.core.gui.SnapAllignDistribute;
@@ -134,7 +134,9 @@ import
org.apache.hop.ui.hopgui.file.delegates.HopGuiNoteLinkSupport;
import org.apache.hop.ui.hopgui.file.delegates.HopGuiNotePadDelegate;
import org.apache.hop.ui.hopgui.file.shared.DrillDownGuiPlugin;
import org.apache.hop.ui.hopgui.file.shared.HopGuiAbstractGraph;
+import org.apache.hop.ui.hopgui.file.shared.HopGuiGraphSnapshotUndo;
import org.apache.hop.ui.hopgui.file.shared.HopGuiTooltipExtension;
+import org.apache.hop.ui.hopgui.file.shared.ISnapshotUndoSupport;
import
org.apache.hop.ui.hopgui.file.workflow.context.HopGuiWorkflowActionContext;
import org.apache.hop.ui.hopgui.file.workflow.context.HopGuiWorkflowContext;
import org.apache.hop.ui.hopgui.file.workflow.context.HopGuiWorkflowHopContext;
@@ -217,7 +219,8 @@ public class HopGuiWorkflowGraph extends HopGuiAbstractGraph
ILogParentProvided,
IHopFileTypeHandler,
IGuiRefresher,
- IWebCanvasGraph {
+ IWebCanvasGraph,
+ ISnapshotUndoSupport {
private static final Class<?> PKG = HopGuiWorkflowGraph.class;
@@ -386,6 +389,7 @@ public class HopGuiWorkflowGraph extends HopGuiAbstractGraph
public HopGuiWorkflowClipboardDelegate workflowClipboardDelegate;
public HopGuiWorkflowRunDelegate workflowRunDelegate;
public HopGuiWorkflowUndoDelegate workflowUndoDelegate;
+ private final HopGuiGraphSnapshotUndo<WorkflowMeta> snapshotUndo;
public HopGuiWorkflowActionDelegate workflowActionDelegate;
public HopGuiWorkflowHopDelegate workflowHopDelegate;
public HopGuiNotePadDelegate notePadDelegate;
@@ -442,6 +446,17 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
workflowClipboardDelegate = new HopGuiWorkflowClipboardDelegate(hopGui,
this);
workflowRunDelegate = new HopGuiWorkflowRunDelegate(hopGui, this);
workflowUndoDelegate = new HopGuiWorkflowUndoDelegate(hopGui, this);
+ snapshotUndo =
+ new HopGuiGraphSnapshotUndo<>(
+ hopGui,
+ WorkflowMeta.class,
+ WorkflowMeta.XML_TAG,
+ (target, node, provider, filename) ->
+ target.restoreContentFromXml(node, filename, provider),
+ () -> this.workflowMeta,
+ this::getFilename,
+ this::restoreAfterSnapshot);
+ snapshotUndo.initialize();
workflowActionDelegate = new HopGuiWorkflowActionDelegate(hopGui, this);
workflowHopDelegate = new HopGuiWorkflowHopDelegate(hopGui, this);
notePadDelegate = new HopGuiNotePadDelegate(hopGui, this);
@@ -796,6 +811,7 @@ public class HopGuiWorkflowGraph extends HopGuiAbstractGraph
// SWT keeps the threshold behaviour to distinguish a click from a
drag.
if (EnvironmentUtils.getInstance().isWeb() && event.button == 1 &&
!shift && !control) {
actionDragCommitted = true;
+ markPositionUndoPoint();
dragSelection = true;
canvas.setData("mode", "drag");
selectedActions = workflowMeta.getSelectedActions();
@@ -1161,31 +1177,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
// Find out which Transforms & Notes are selected
selectedActions = workflowMeta.getSelectedActions();
selectedNotes = workflowMeta.getSelectedNotes();
- // We moved around some items: store undo info...
- //
- boolean also = false;
- if (!Utils.isEmpty(selectedNotes) && previousNoteLocations != null) {
- int[] indexes = workflowMeta.getNoteIndexes(selectedNotes);
-
- addUndoPosition(
- selectedNotes.toArray(new NotePadMeta[selectedNotes.size()]),
- indexes,
- previousNoteLocations,
- workflowMeta.getSelectedNoteLocations(),
- also);
- also = !Utils.isEmpty(selectedActions);
- }
- if (selectedActions != null
- && !selectedActions.isEmpty()
- && previousActionLocations != null) {
- int[] indexes = workflowMeta.getActionIndexes(selectedActions);
- addUndoPosition(
- selectedActions.toArray(new
ActionMeta[selectedActions.size()]),
- indexes,
- previousActionLocations,
- workflowMeta.getSelectedLocations(),
- also);
- }
+ // Position undo was recorded at drag start via
markPositionUndoPoint().
}
}
@@ -1231,6 +1223,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
endHopLocation = null;
actionDragStartScreen = null;
actionDragCommitted = false;
+ resetPositionUndoMark();
removePlacementDragFilters();
updateGui();
@@ -1264,31 +1257,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
// Track that actions/notes were selected
if (!selectedActions.isEmpty() || !selectedNotes.isEmpty()) {}
- // We moved around some items: store undo info...
- boolean also = false;
- if (selectedNotes != null
- && !selectedNotes.isEmpty()
- && previousNoteLocations != null) {
- int[] indexes = workflowMeta.getNoteIndexes(selectedNotes);
- addUndoPosition(
- selectedNotes.toArray(new NotePadMeta[selectedNotes.size()]),
- indexes,
- previousNoteLocations,
- workflowMeta.getSelectedNoteLocations(),
- also);
- also = !Utils.isEmpty(selectedActions);
- }
- if (selectedActions != null
- && !selectedActions.isEmpty()
- && previousActionLocations != null) {
- int[] indexes = workflowMeta.getActionIndexes(selectedActions);
- addUndoPosition(
- selectedActions.toArray(new
ActionMeta[selectedActions.size()]),
- indexes,
- previousActionLocations,
- workflowMeta.getSelectedLocations(),
- also);
- }
+ // Position undo was recorded at drag start via
markPositionUndoPoint().
}
}
@@ -1956,6 +1925,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
int thresholdSq = ACTION_DRAG_THRESHOLD_PX * ACTION_DRAG_THRESHOLD_PX;
if (dx * dx + dy * dy > thresholdSq) {
actionDragCommitted = true;
+ markPositionUndoPoint();
canvas.setData("mode", "drag");
dragSelection = true;
selectedActions = workflowMeta.getSelectedActions();
@@ -2095,6 +2065,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
*
* new : new position of the note (not the mouse pointer) dx :
difference with previous position
*/
+ markPositionUndoPoint();
int dx = note.x - selectedNote.getLocation().x;
int dy = note.y - selectedNote.getLocation().y;
@@ -2311,48 +2282,18 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
*/
private void applyAutoLayout(boolean selectionOnly) {
List<ActionMeta> subset = null;
- List<ActionMeta> moving;
if (selectionOnly) {
subset = workflowMeta.getSelectedActions();
if (subset == null || subset.size() < 2) {
return; // Nothing meaningful to arrange.
}
- moving = new ArrayList<>(subset);
- } else {
- int n = workflowMeta.nrActions();
- if (n == 0) {
- return;
- }
- moving = new ArrayList<>(n);
- for (int i = 0; i < n; i++) {
- moving.add(workflowMeta.getAction(i));
- }
+ } else if (workflowMeta.nrActions() == 0) {
+ return;
}
- // Auto-layout may also reposition notes; capture them so the whole thing
is one undo step.
- List<NotePadMeta> notes = new ArrayList<>(workflowMeta.getNotes());
- Point[] notesBefore = captureNoteLocations(notes);
-
- Point[] before = captureLocations(moving);
+ byte[] beforeSnapshot = captureUndoSnapshot();
WorkflowMetaLayout.layout(workflowMeta,
PropsUi.getInstance().getAutoLayoutOptions(), subset);
- Point[] after = captureLocations(moving);
- Point[] notesAfter = captureNoteLocations(notes);
-
- // Record notes first, then actions, linked into a single undo action
(nextAlso).
- boolean also = false;
- if (!notes.isEmpty()) {
- also = true;
- hopGui.undoDelegate.addUndoPosition(
- workflowMeta,
- notes.toArray(new NotePadMeta[0]),
- workflowMeta.getNoteIndexes(notes),
- notesBefore,
- notesAfter,
- also);
- }
- int[] indexes = workflowMeta.getActionIndexes(moving);
- hopGui.undoDelegate.addUndoPosition(
- workflowMeta, moving.toArray(new ActionMeta[0]), indexes, before,
after, also);
+ commitDialogUndo(beforeSnapshot);
workflowMeta.setChanged();
updateGui();
@@ -3637,6 +3578,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
Point[] actionsBefore = captureLocations(actions);
Point[] notesBefore = captureNoteLocations(notes);
+ byte[] beforeSnapshot = captureUndoSnapshot();
moveSelected(dx, dy);
@@ -3647,26 +3589,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
return true;
}
- // Record notes first, then actions, linked into a single undo action
(nextAlso).
- boolean also = false;
- if (!Utils.isEmpty(notes)) {
- also = !Utils.isEmpty(actions);
- addUndoPosition(
- notes.toArray(new NotePadMeta[0]),
- workflowMeta.getNoteIndexes(notes),
- notesBefore,
- notesAfter,
- also);
- }
- if (!Utils.isEmpty(actions)) {
- addUndoPosition(
- actions.toArray(new ActionMeta[0]),
- workflowMeta.getActionIndexes(actions),
- actionsBefore,
- actionsAfter,
- also);
- }
-
+ commitDialogUndo(beforeSnapshot);
workflowMeta.setChanged();
updateGui();
return true;
@@ -4271,7 +4194,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
public SnapAllignDistribute createSnapAlignDistribute() {
List<ActionMeta> elements = workflowMeta.getSelectedActions();
int[] indices = workflowMeta.getActionIndexes(elements);
- return new SnapAllignDistribute(workflowMeta, elements, indices,
hopGui.undoDelegate, this);
+ return new SnapAllignDistribute(workflowMeta, elements, indices, null,
this);
}
@GuiContextAction(
@@ -4350,6 +4273,66 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
}
}
+ @Override
+ public boolean isUndoMeta(IUndo undoInterface) {
+ return undoInterface == workflowMeta;
+ }
+
+ @Override
+ public void markUndoPoint() {
+ snapshotUndo.markUndoPoint();
+ }
+
+ @Override
+ public byte[] captureUndoSnapshot() {
+ return snapshotUndo.captureUndoSnapshot();
+ }
+
+ @Override
+ public void commitDialogUndo(byte[] before) {
+ snapshotUndo.commitDialogUndo(before);
+ }
+
+ @Override
+ public void recordAfterChange(boolean nextAlso) {
+ snapshotUndo.recordAfterChange(nextAlso);
+ }
+
+ @Override
+ public void markPositionUndoPoint() {
+ snapshotUndo.markPositionUndoPoint();
+ }
+
+ @Override
+ public void resetPositionUndoMark() {
+ snapshotUndo.resetPositionUndoMark();
+ }
+
+ @Override
+ public void rememberSavedSnapshot() {
+ snapshotUndo.rememberSavedSnapshot();
+ }
+
+ @Override
+ public boolean canUndo() {
+ return snapshotUndo.canUndo();
+ }
+
+ @Override
+ public boolean canRedo() {
+ return snapshotUndo.canRedo();
+ }
+
+ private void restoreAfterSnapshot() {
+ if (workflowMeta != null) {
+ workflowMeta.setInternalHopVariables(variables);
+ }
+ clearSettings();
+ snapshotUndo.resetPositionUndoMark();
+ updateGui();
+ redraw();
+ }
+
@GuiToolbarElement(
root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
id = TOOLBAR_ITEM_UNDO_ID,
@@ -4361,7 +4344,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
@GuiOsxKeyboardShortcut(command = true, key = 'z')
@Override
public void undo() {
- workflowUndoDelegate.undoWorkflowAction(this, workflowMeta);
+ snapshotUndo.undo();
forceFocus();
}
@@ -4375,7 +4358,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
@GuiOsxKeyboardShortcut(command = true, shift = true, key = 'z')
@Override
public void redo() {
- workflowUndoDelegate.redoWorkflowAction(this, workflowMeta);
+ snapshotUndo.redo();
forceFocus();
}
@@ -4413,10 +4396,9 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
// Enable/disable the undo/redo toolbar buttons...
//
- toolBarWidgets.enableToolbarItem(
- TOOLBAR_ITEM_UNDO_ID, workflowMeta.viewThisUndo() != null);
- toolBarWidgets.enableToolbarItem(
- TOOLBAR_ITEM_REDO_ID, workflowMeta.viewNextUndo() != null);
+ snapshotUndo.refreshLastSnapshot();
+ toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_UNDO_ID,
snapshotUndo.canUndo());
+ toolBarWidgets.enableToolbarItem(TOOLBAR_ITEM_REDO_ID,
snapshotUndo.canRedo());
// Enable/disable the execution toolbar buttons
//
@@ -4438,7 +4420,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
toolBarWidgets.enableToolbarItem(
TOOLBAR_ITEM_TO_EXECUTION_INFO, hasExecutionInfoLocations);
- hopGui.setUndoMenu(workflowMeta);
+ hopGui.setUndoMenu(snapshotUndo.canUndo(),
snapshotUndo.canRedo());
hopGui.handleFileCapabilities(fileType,
workflowMeta.hasChanged(), running, false);
// Enable the align/distribute toolbar menus if one or more
actions are selected.
@@ -4533,6 +4515,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
out.write(XmlHandler.getXmlHeader(Const.UTF_8).getBytes(StandardCharsets.UTF_8));
out.write(xml.getBytes(StandardCharsets.UTF_8));
workflowMeta.clearChanged();
+ rememberSavedSnapshot();
updateGui();
} finally {
out.flush();
@@ -5231,17 +5214,14 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
return () -> getWorkflow() != null ? getWorkflow().getLogChannel() :
LogChannel.GENERAL;
}
- // Change of transform, connection, hop or note...
public void addUndoPosition(Object[] obj, int[] pos, Point[] prev, Point[]
curr) {
addUndoPosition(obj, pos, prev, curr, false);
}
- // Change of transform, connection, hop or note...
public void addUndoPosition(
Object[] obj, int[] pos, Point[] prev, Point[] curr, boolean nextAlso) {
- // It's better to store the indexes of the objects, not the objects itself!
- workflowMeta.addUndo(obj, null, pos, prev, curr,
AbstractMeta.TYPE_UNDO_POSITION, nextAlso);
- hopGui.setUndoMenu(workflowMeta);
+ recordAfterChange(nextAlso);
+ hopGui.setUndoMenu(canUndo(), canRedo());
}
/**
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowActionDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowActionDelegate.java
index 22247f2081..3d854252a4 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowActionDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowActionDelegate.java
@@ -291,7 +291,7 @@ public class HopGuiWorkflowActionDelegate {
return;
}
- ActionMeta before = (ActionMeta) action.cloneDeep();
+ byte[] beforeSnapshot = workflowGraph.captureUndoSnapshot();
IAction jei = action.getAction();
@@ -304,13 +304,7 @@ public class HopGuiWorkflowActionDelegate {
// If so, we need to verify that the name is not already used in the
workflow.
//
workflowMeta.renameActionIfNameCollides(action);
-
- ActionMeta after = action.clone();
- hopGui.undoDelegate.addUndoChange(
- workflowMeta,
- new ActionMeta[] {before},
- new ActionMeta[] {after},
- new int[] {workflowMeta.indexOfAction(action)});
+ workflowGraph.commitDialogUndo(beforeSnapshot);
}
workflowGraph.updateGui();
} else {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowClipboardDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowClipboardDelegate.java
index 833f1af46c..3298882611 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowClipboardDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowClipboardDelegate.java
@@ -158,8 +158,7 @@ public class HopGuiWorkflowClipboardDelegate {
// This is the offset:
Point offset = new Point(location.x - min.x, location.y - min.y);
- // Undo/redo object positions...
- int[] position = new int[actions.length];
+ workflowGraph.markUndoPoint();
for (int i = 0; i < actions.length; i++) {
Point p = actions[i].getLocation();
@@ -170,7 +169,6 @@ public class HopGuiWorkflowClipboardDelegate {
actionsOldNames.add(name);
actions[i].setName(workflowMeta.getAlternativeActionName(name));
workflowMeta.addAction(actions[i]);
- position[i] = workflowMeta.indexOfAction(actions[i]);
actions[i].setSelected(true);
}
@@ -188,20 +186,7 @@ public class HopGuiWorkflowClipboardDelegate {
note.setSelected(true);
}
- // Save undo information too...
- hopGui.undoDelegate.addUndoNew(workflowMeta, actions, position, false);
-
- int[] hopPos = new int[hops.length];
- for (int i = 0; i < hops.length; i++) {
- hopPos[i] = workflowMeta.indexOfWorkflowHop(hops[i]);
- }
- hopGui.undoDelegate.addUndoNew(workflowMeta, hops, hopPos, true);
-
- int[] notePos = new int[notes.length];
- for (int i = 0; i < notes.length; i++) {
- notePos[i] = workflowMeta.indexOfNote(notes[i]);
- }
- hopGui.undoDelegate.addUndoNew(workflowMeta, notes, notePos, true);
+ // Undo was recorded once before adding the pasted objects.
} catch (HopException e) {
// See if this was different (non-XML) content
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowUndoDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowUndoDelegate.java
index 7409b17134..f7872677fa 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowUndoDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowUndoDelegate.java
@@ -17,337 +17,45 @@
package org.apache.hop.ui.hopgui.file.workflow.delegates;
-import org.apache.hop.core.NotePadMeta;
-import org.apache.hop.core.gui.Point;
-import org.apache.hop.core.undo.ChangeAction;
import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
import org.apache.hop.ui.hopgui.file.workflow.HopGuiWorkflowGraph;
-import org.apache.hop.workflow.WorkflowHopMeta;
import org.apache.hop.workflow.WorkflowMeta;
-import org.apache.hop.workflow.action.ActionMeta;
+/**
+ * Workflow undo/redo is implemented with gzip XML snapshots on {@link
HopGuiWorkflowGraph}. This
+ * class remains as a hook for any leftover callers.
+ */
public class HopGuiWorkflowUndoDelegate {
private HopGuiWorkflowGraph workflowGraph;
private HopGui hopGui;
- /**
- * @param hopGui
- */
public HopGuiWorkflowUndoDelegate(HopGui hopGui, HopGuiWorkflowGraph
workflowGraph) {
this.hopGui = hopGui;
this.workflowGraph = workflowGraph;
}
public void undoWorkflowAction(IHopFileTypeHandler handler, WorkflowMeta
workflowMeta) {
- ChangeAction changeAction = workflowMeta.previousUndo();
- if (changeAction == null) {
- return;
- }
- undoWorkflowAction(handler, workflowMeta, changeAction);
- handler.updateGui();
- }
-
- public void undoWorkflowAction(
- IHopFileTypeHandler handler, WorkflowMeta workflowMeta, ChangeAction
changeAction) {
- switch (changeAction.getType()) {
- // We created a new transform : undo this...
- case NewAction:
- // Delete the transform at correct location:
- for (int i = changeAction.getCurrent().length - 1; i >= 0; i--) {
- int idx = changeAction.getCurrentIndex()[i];
- workflowMeta.removeAction(idx);
- }
- break;
-
- // We created a new note : undo this...
- case NewNote:
- // Delete the note at correct location:
- for (int i = changeAction.getCurrent().length - 1; i >= 0; i--) {
- int idx = changeAction.getCurrentIndex()[i];
- workflowMeta.removeNote(idx);
- }
- break;
-
- // We created a new hop : undo this...
- case NewWorkflowHop:
- // Delete the hop at correct location:
- for (int i = changeAction.getCurrent().length - 1; i >= 0; i--) {
- int idx = changeAction.getCurrentIndex()[i];
- workflowMeta.removeWorkflowHop(idx);
- }
- break;
-
- //
- // DELETE
- //
-
- // We delete a transform : undo this...
- case DeleteAction:
- // un-Delete the transform at correct location: re-insert
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- ActionMeta action = (ActionMeta) changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
- workflowMeta.addAction(idx, action);
- }
- break;
-
- // We delete new note : undo this...
- case DeleteNote:
- // re-insert the note at correct location:
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- NotePadMeta ni = (NotePadMeta) changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
- workflowMeta.addNote(idx, ni);
- }
- break;
-
- // We deleted a hop : undo this...
- case DeleteWorkflowHop:
- // re-insert the hop at correct location:
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- WorkflowHopMeta hopMeta = (WorkflowHopMeta)
changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
- // Build a new hop:
- ActionMeta from =
workflowMeta.findAction(hopMeta.getFromAction().getName());
- ActionMeta to =
workflowMeta.findAction(hopMeta.getToAction().getName());
- WorkflowHopMeta newHopMeta = new WorkflowHopMeta(from, to);
- newHopMeta.setEvaluation(hopMeta.isEvaluation());
- newHopMeta.setUnconditional(hopMeta.isUnconditional());
- workflowMeta.addWorkflowHop(idx, newHopMeta);
- }
- break;
-
- //
- // CHANGE
- //
-
- // We changed a transform : undo this...
- case ChangeAction:
- // Delete the current transform, insert previous version.
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- ActionMeta prev = ((ActionMeta)
changeAction.getPrevious()[i]).clone();
- int idx = changeAction.getCurrentIndex()[i];
-
- workflowMeta.getAction(idx).replaceMeta(prev);
- }
- break;
-
- // We changed a note : undo this...
- case ChangeNote:
- // Delete & re-insert
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- int idx = changeAction.getCurrentIndex()[i];
- workflowMeta.removeNote(idx);
- NotePadMeta prev = (NotePadMeta) changeAction.getPrevious()[i];
- workflowMeta.addNote(idx, (NotePadMeta) prev.clone());
- }
- break;
-
- // We changed a hop : undo this...
- case ChangeWorkflowHop:
- // Delete & re-insert
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- WorkflowHopMeta prev = (WorkflowHopMeta)
changeAction.getPrevious()[i];
- int idx = changeAction.getCurrentIndex()[i];
-
- workflowMeta.removeWorkflowHop(idx);
- workflowMeta.addWorkflowHop(idx, (WorkflowHopMeta) prev.clone());
- }
- break;
-
- //
- // POSITION
- //
-
- // The position of a transform has changed: undo this...
- case PositionAction:
- // Find the location of the transform:
- for (int i = 0; i < changeAction.getCurrentIndex().length; i++) {
- ActionMeta action =
workflowMeta.getAction(changeAction.getCurrentIndex()[i]);
- action.setLocation(changeAction.getPreviousLocation()[i]);
- }
- break;
-
- // The position of a note has changed: undo this...
- case PositionNote:
- for (int i = 0; i < changeAction.getCurrentIndex().length; i++) {
- int idx = changeAction.getCurrentIndex()[i];
- NotePadMeta npi = workflowMeta.getNote(idx);
- Point prev = changeAction.getPreviousLocation()[i];
- npi.setLocation(prev);
- }
- break;
- default:
- break;
- }
-
- // OK, now check if we need to do this again...
- if (workflowMeta.viewNextUndo() != null &&
workflowMeta.viewNextUndo().getNextAlso()) {
- undoWorkflowAction(handler, workflowMeta);
- }
+ workflowGraph.undo();
}
public void redoWorkflowAction(IHopFileTypeHandler handler, WorkflowMeta
workflowMeta) {
- ChangeAction changeAction = workflowMeta.nextUndo();
- if (changeAction == null) {
- return;
- }
- redoWorkflowAction(handler, workflowMeta, changeAction);
- handler.updateGui();
- }
-
- public void redoWorkflowAction(
- IHopFileTypeHandler handler, WorkflowMeta workflowMeta, ChangeAction
changeAction) {
- switch (changeAction.getType()) {
- case NewAction:
- // re-delete the transform at correct location:
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- ActionMeta entryCopy = (ActionMeta) changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
- workflowMeta.addAction(idx, entryCopy);
- }
- break;
-
- case NewNote:
- // re-insert the note at correct location:
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- NotePadMeta ni = (NotePadMeta) changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
- workflowMeta.addNote(idx, ni);
- }
- break;
-
- case NewWorkflowHop:
- // re-insert the hop at correct location:
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- WorkflowHopMeta hopMeta = (WorkflowHopMeta)
changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
- workflowMeta.addWorkflowHop(idx, hopMeta);
- }
- break;
-
- //
- // DELETE
- //
- case DeleteAction:
- // re-remove the transform at correct location:
- for (int i = changeAction.getCurrent().length - 1; i >= 0; i--) {
- int idx = changeAction.getCurrentIndex()[i];
- workflowMeta.removeAction(idx);
- }
- break;
-
- case DeleteNote:
- // re-remove the note at correct location:
- for (int i = changeAction.getCurrent().length - 1; i >= 0; i--) {
- int idx = changeAction.getCurrentIndex()[i];
- workflowMeta.removeNote(idx);
- }
- break;
-
- case DeleteWorkflowHop:
- // re-remove the hop at correct location:
- for (int i = changeAction.getCurrent().length - 1; i >= 0; i--) {
- int idx = changeAction.getCurrentIndex()[i];
- workflowMeta.removeWorkflowHop(idx);
- }
- break;
-
- //
- // CHANGE
- //
-
- // We changed a transform : undo this...
- case ChangeTransform:
- // Delete the current transform, insert previous version.
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- ActionMeta clonedEntry = ((ActionMeta)
changeAction.getCurrent()[i]).clone();
-
workflowMeta.getAction(changeAction.getCurrentIndex()[i]).replaceMeta(clonedEntry);
- }
- break;
-
- // We changed a note : undo this...
- case ChangeNote:
- // Delete & re-insert
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- NotePadMeta ni = (NotePadMeta) changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
-
- workflowMeta.removeNote(idx);
- workflowMeta.addNote(idx, ni.clone());
- }
- break;
-
- // We changed a hop : undo this...
- case ChangeWorkflowHop:
- // Delete & re-insert
- for (int i = 0; i < changeAction.getCurrent().length; i++) {
- WorkflowHopMeta hi = (WorkflowHopMeta) changeAction.getCurrent()[i];
- int idx = changeAction.getCurrentIndex()[i];
-
- workflowMeta.removeWorkflowHop(idx);
- workflowMeta.addWorkflowHop(idx, (WorkflowHopMeta) hi.clone());
- }
- break;
-
- //
- // CHANGE POSITION
- //
- case PositionAction:
- for (int i = 0; i < changeAction.getCurrentIndex().length; i++) {
- // Find & change the location of the transform:
- ActionMeta action =
workflowMeta.getAction(changeAction.getCurrentIndex()[i]);
- action.setLocation(changeAction.getCurrentLocation()[i]);
- }
- break;
- case PositionNote:
- for (int i = 0; i < changeAction.getCurrentIndex().length; i++) {
- int idx = changeAction.getCurrentIndex()[i];
- NotePadMeta npi = workflowMeta.getNote(idx);
- Point curr = changeAction.getCurrentLocation()[i];
- npi.setLocation(curr);
- }
- break;
- default:
- break;
- }
-
- // OK, now check if we need to do this again...
- if (workflowMeta.viewNextUndo() != null &&
workflowMeta.viewNextUndo().getNextAlso()) {
- redoWorkflowAction(handler, workflowMeta);
- }
+ workflowGraph.redo();
}
- /**
- * Gets workflowGraph
- *
- * @return value of workflowGraph
- */
public HopGuiWorkflowGraph getWorkflowGraph() {
return workflowGraph;
}
- /**
- * @param workflowGraph The workflowGraph to set
- */
public void setWorkflowGraph(HopGuiWorkflowGraph workflowGraph) {
this.workflowGraph = workflowGraph;
}
- /**
- * Gets hopGui
- *
- * @return value of hopGui
- */
public HopGui getHopGui() {
return hopGui;
}
- /**
- * @param hopGui The hopGui to set
- */
public void setHopGui(HopGui hopGui) {
this.hopGui = hopGui;
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigPlugin.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigPlugin.java
index 13c2ab7cf2..9c6218dcf4 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigPlugin.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigPlugin.java
@@ -17,6 +17,7 @@
package org.apache.hop.ui.hopgui.perspective.explorer.config;
+import org.apache.hop.core.Const;
import org.apache.hop.core.config.plugin.ConfigPlugin;
import org.apache.hop.core.config.plugin.IConfigOptions;
import org.apache.hop.core.exception.HopException;
@@ -52,6 +53,7 @@ public class ExplorerPerspectiveConfigPlugin
"10200-file-explorer-visible-by-default";
private static final String WIDGET_ID_OPEN_HELP_FILES =
"10300-open-help-files";
private static final String WIDGET_ID_ACTIVE_FILE_SELECTION =
"10400-active-file-selection";
+ private static final String WIDGET_ID_MAX_UNDO = "10500-max-undo";
@GuiWidgetElement(
id = WIDGET_ID_LAZY_LOADING_DEPTH,
@@ -110,6 +112,18 @@ public class ExplorerPerspectiveConfigPlugin
description = "Automatically select the active tab file in the file
explorer tree")
private Boolean activeFileSelection = true;
+ @GuiWidgetElement(
+ id = WIDGET_ID_MAX_UNDO,
+ parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
+ type = GuiElementType.TEXT,
+ label = "i18n::ExplorerPerspectiveConfig.MaxUndo.Label",
+ toolTip = "i18n::ExplorerPerspectiveConfig.MaxUndo.Tooltip")
+ @CommandLine.Option(
+ names = {"-mu", "--max-undo"},
+ description =
+ "The maximum number of undo operations kept for pipelines, workflows
and tables")
+ private String maxUndo;
+
/**
* Gets instance
*
@@ -125,6 +139,7 @@ public class ExplorerPerspectiveConfigPlugin
instance.fileExplorerVisibleByDefault = visibleByDefault != null ?
visibleByDefault : true;
instance.openingHelpFiles = config.isOpeningHelpFiles();
instance.activeFileSelection = config.getActiveFileSelection();
+ instance.maxUndo =
Integer.toString(org.apache.hop.ui.core.PropsUi.getInstance().getMaxUndo());
return instance;
}
@@ -176,6 +191,12 @@ public class ExplorerPerspectiveConfigPlugin
changed = true;
}
+ if (maxUndo != null) {
+ persistMaxUndo(maxUndo);
+ log.logBasic("Maximum undo operations is set to '" + maxUndo + "'");
+ changed = true;
+ }
+
// Save to file if anything changed
//
if (changed) {
@@ -230,6 +251,14 @@ public class ExplorerPerspectiveConfigPlugin
ExplorerPerspectiveConfigSingleton.getConfig()
.setActiveFileSelection(activeFileSelection);
break;
+ case WIDGET_ID_MAX_UNDO:
+ if (control instanceof TextVar textVar) {
+ maxUndo = textVar.getText();
+ } else {
+ maxUndo = ((org.eclipse.swt.widgets.Text) control).getText();
+ }
+ persistMaxUndo(maxUndo);
+ break;
default:
break;
}
@@ -282,4 +311,20 @@ public class ExplorerPerspectiveConfigPlugin
public void setActiveFileSelection(Boolean activeFileSelection) {
this.activeFileSelection = activeFileSelection;
}
+
+ public String getMaxUndo() {
+ return maxUndo;
+ }
+
+ public void setMaxUndo(String maxUndo) {
+ this.maxUndo = maxUndo;
+ }
+
+ private static void persistMaxUndo(String maxUndoText) {
+ int value = Const.toInt(maxUndoText, Const.MAX_UNDO);
+ if (value < 1) {
+ value = 1;
+ }
+ org.apache.hop.ui.core.PropsUi.getInstance().setMaxUndo(value);
+ }
}
diff --git
a/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
index 0aac9a2983..a9fa08b436 100644
---
a/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/hopgui/messages/messages_en_US.properties
@@ -133,6 +133,10 @@ HopGui.Menu.Tools.DatabaseClearCache=Clear databases cache
HopGui.Menu.Tools.EditConfigVariables=Edit config variables...
HopGui.Menu.Undo.Available=Undo \: {0}
HopGui.Menu.Undo.NotAvailable=Undo \: not available
+HopGui.Undo.Error.Apply.Message=Unable to apply the undo/redo snapshot
+HopGui.Undo.Error.Apply.Title=Undo error
+HopGui.Undo.Error.Record.Message=Unable to record undo snapshot
+HopGui.Undo.Error.Record.Title=Undo error
HopGui.Message.Warning.NotShowWarning=Please, don''t show this warning anymore
(also available in the options dialog).
HopGui.PipelineExecutionConfigurationDialog.Help=pipeline/run-preview-debug-pipeline.html
HopGui.PipelineGraph.GridTab.Name=Metrics
diff --git
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_en_US.properties
index b19a2b9820..8147d0c3a2 100644
---
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/config/messages/messages_en_US.properties
@@ -28,3 +28,6 @@ ExplorerPerspectiveConfig.OpenHelpFiles.Tooltip=When checked,
help links will op
ExplorerPerspectiveConfig.ActiveFileSelection.Label=Select active file in tree
automatically
ExplorerPerspectiveConfig.ActiveFileSelection.Tooltip=Automatically select the
active tab file in the file explorer tree on the left hand side when it is
shown in a tab.
+ExplorerPerspectiveConfig.MaxUndo.Label=Maximum undo operations
+ExplorerPerspectiveConfig.MaxUndo.Tooltip=The maximum number of undo/redo
snapshots kept for pipelines, workflows and table editors. Default is 100.
+