【问题标题】:UndoFX: Undo/Redo recording every pixel of dragUndoFX:撤消/重做记录拖动的每个像素
【发布时间】:2016-05-24 14:49:02
【问题描述】:

我正在使用 UndoFX 和 ReactFX 为我的 2D 形状应用程序实现 Undo/Redo 功能。

问题是当我移动我的形状时,EventStream 会记录移动的每个 X/Y 像素。我只想记录最后一个位置(当用户释放拖动时)。

到目前为止我所尝试的:

而不是使用changesOf(rect.xProperty()).map(c -> new xChange(c));changesOf(rect.yProperty()).map(c -> new yChange(c)); 我创建了一个DoubleProperty x,y,并在释放用户鼠标时将形状 x,y 属性保存到这些变量中。 最后,我将 changesOf 更改为:changesOf(this.x).map(c -> new xChange(c));changesOf(this.y).map(c -> new yChange(c));

但这不起作用,它的行为和以前一样。

....
private class xChange extends RectangleChange<Double> {

    public xChange(Double oldValue, Double newValue) {
        super(oldValue, newValue);
    }
    public xChange(Change<Number> c) {
        super(c.getOldValue().doubleValue(), c.getNewValue().doubleValue());
    }
    @Override void redo() { rect.setX(newValue); }
    @Override xChange invert() { return new xChange(newValue, oldValue); }
    @Override Optional<RectangleChange<?>> mergeWith(RectangleChange<?> other) {
        if(other instanceof xChange) {
            return Optional.of(new xChange(oldValue, ((xChange) other).newValue));
        } else {
            return Optional.empty();
        }
    }

    @Override
    public boolean equals(Object other) {
        if(other instanceof xChange) {
            xChange that = (xChange) other;
            return Objects.equals(this.oldValue, that.oldValue)
                && Objects.equals(this.newValue, that.newValue);
        } else {
            return false;
        }
    }
}

...
    EventStream<xChange> xChanges = changesOf(rect.xProperty()).map(c -> new xChange(c));
    EventStream<yChange> yChanges = changesOf(rect.yProperty()).map(c -> new yChange(c));
    changes = merge(widthChanges, heightChanges, xChanges, yChanges);
    undoManager = UndoManagerFactory.unlimitedHistoryUndoManager(
            changes, // stream of changes to observe
            c -> c.invert(), // function to invert a change
            c -> c.redo(), // function to undo a change
            (c1, c2) -> c1.mergeWith(c2)); // function to merge two changes

【问题讨论】:

  • 我建议合并相同类型的后续更改(此处移动形状)为一个更改。它在demo 中得到了演示(参见CenterXChangeCenterYChange 中的mergeWith 方法)。
  • 我刚试过,没有用 :( 再次出现相同的行为!实际上,确切的问题是当我同时水平和垂直移动形状时。如果我将它完全移动到一个方向,那么它的工作原理很好。
  • 哇,我刚刚意识到你是创建 UndoFX 的人:D。感谢您的工作伙伴!

标签: javafx reactfx


【解决方案1】:

您需要将 x 中的更改与 y 中的更改合并。目前,x 的变化和 y 的变化不能合并,因此如果移动形状以使其交替 x 和 y 变化(例如沿对角线移动),那么每个单独的变化都不会与前一个变化合并。

执行此操作的一种方法是生成以旧值和新值作为位置的更改,例如由Point2D 对象表示。这是一个简单的例子:

import java.util.Objects;
import java.util.Optional;

import org.fxmisc.undo.UndoManager;
import org.fxmisc.undo.UndoManagerFactory;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.SuspendableEventStream;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class UndoRectangle extends Application {



    @Override
    public void start(Stage primaryStage) {
        Rectangle rect = new Rectangle(50, 50, 150, 100);
        rect.setFill(Color.CORNFLOWERBLUE);

        EventStream<PositionChange> xChanges = EventStreams.changesOf(rect.xProperty()).map(c -> {
            double oldX = c.getOldValue().doubleValue();
            double newX = c.getNewValue().doubleValue();
            double y = rect.getY();
            return new PositionChange(new Point2D(oldX, y), new Point2D(newX, y));
        });

        EventStream<PositionChange> yChanges = EventStreams.changesOf(rect.yProperty()).map(c -> {
            double oldY = c.getOldValue().doubleValue();
            double newY = c.getNewValue().doubleValue();
            double x = rect.getX();
            return new PositionChange(new Point2D(x, oldY), new Point2D(x, newY));
        });

        SuspendableEventStream<PositionChange> posChanges = EventStreams.merge(xChanges, yChanges)
                .reducible(PositionChange::merge);

        UndoManager undoManager = UndoManagerFactory.unlimitedHistoryUndoManager(posChanges, 
            PositionChange::invert,
            c -> posChanges.suspendWhile(() -> {
                rect.setX(c.getNewPosition().getX());
                rect.setY(c.getNewPosition().getY());
            }),
            (c1, c2) -> Optional.of(c1.merge(c2))
        );

        class MouseLoc { double x, y ; }

        MouseLoc mouseLoc = new MouseLoc();

        rect.setOnMousePressed(e -> {
            mouseLoc.x = e.getSceneX();
            mouseLoc.y = e.getSceneY();
        });

        rect.setOnMouseDragged(e -> {
            rect.setX(rect.getX() + e.getSceneX() - mouseLoc.x);
            rect.setY(rect.getY() + e.getSceneY() - mouseLoc.y);
            mouseLoc.x = e.getSceneX();
            mouseLoc.y = e.getSceneY();
        });

        rect.setOnMouseReleased(e -> undoManager.preventMerge());

        Pane pane = new Pane(rect);

        Button undo = new Button("Undo");
        undo.disableProperty().bind(Bindings.not(undoManager.undoAvailableProperty()));
        undo.setOnAction(e -> undoManager.undo());

        Button redo = new Button("Redo");
        redo.disableProperty().bind(Bindings.not(undoManager.redoAvailableProperty()));
        redo.setOnAction(e -> undoManager.redo());

        HBox buttons = new HBox(5, undo, redo);
        buttons.setAlignment(Pos.CENTER);
        BorderPane.setMargin(buttons, new Insets(5));
        BorderPane root = new BorderPane(pane, null, null, buttons, null);

        Scene scene = new Scene(root, 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static class PositionChange {
        private final Point2D oldPosition ;
        private final Point2D newPosition ;

        public PositionChange(Point2D oldPos, Point2D newPos) {
            this.oldPosition = oldPos ;
            this.newPosition = newPos ;
        }

        public Point2D getOldPosition() {
            return oldPosition;
        }

        public Point2D getNewPosition() {
            return newPosition;
        }

        public PositionChange merge(PositionChange other) {
            return new PositionChange(oldPosition, other.newPosition);
        }

        public PositionChange invert() {
            return new PositionChange(newPosition, oldPosition);
        }

        @Override
        public boolean equals(Object o) {
            if (o instanceof PositionChange) {
                PositionChange other = (PositionChange) o ;
                return Objects.equals(oldPosition, other.oldPosition)
                        && Objects.equals(newPosition, other.newPosition);
            } else return false ;
        }

        @Override
        public int hashCode() {
            return Objects.hash(oldPosition, newPosition);
        }

    }

    public static void main(String[] args) {
        launch(args);
    }
}

请注意,将“撤消”实现为“原子”更改很重要,因此撤消管理器会在您实现撤消时看到(并忽略)单个更改。这可以通过在撤消期间暂停事件流来实现。

【讨论】:

  • 非常感谢,这样确实可以正常工作。尽管我在将 EventStream 的宽度和高度包含到 undoManager 时遇到了一些困难。
  • 做同样的事情,即创建一个DimensionChange并将宽度和高度属性的事件流合并到DimensionChanges的事件流中。 (或者,如果您想合并位置和尺寸的变化,请创建类似 BoundsChange 的东西,它封装了 x、y、宽度和高度。)
  • 与以往一样,Tomas 的 API 玩起来很有趣。 Here's a fuller example,拖动矩形并调整大小,动画撤消/重做,重新定位和调整大小都被视为 BoundingBox 的变化。
  • 伙计,你可能是 stackoverflow 中最有帮助的人。非常感谢您提供简单而干净的解决方案!登顶! :)
猜你喜欢
  • 1970-01-01
  • 2013-03-25
  • 1970-01-01
  • 2012-03-09
  • 1970-01-01
  • 2014-04-15
  • 2015-06-04
  • 2017-06-28
  • 1970-01-01
相关资源
最近更新 更多