diff --git a/core/src/main/java/edu/wpi/grip/core/events/ConnectionAddedEvent.java b/core/src/main/java/edu/wpi/grip/core/events/ConnectionAddedEvent.java index b19734b7f4..ec67be3957 100644 --- a/core/src/main/java/edu/wpi/grip/core/events/ConnectionAddedEvent.java +++ b/core/src/main/java/edu/wpi/grip/core/events/ConnectionAddedEvent.java @@ -10,7 +10,7 @@ * An event that occurs when a new connection is added to the pipeline. This is triggered by the * user adding a connection with the GUI. */ -public class ConnectionAddedEvent implements RunPipelineEvent { +public class ConnectionAddedEvent implements RunPipelineEvent, DirtiesSaveEvent { private final Connection connection; /** diff --git a/core/src/main/java/edu/wpi/grip/core/events/ConnectionRemovedEvent.java b/core/src/main/java/edu/wpi/grip/core/events/ConnectionRemovedEvent.java index a5bc37eefa..a1165f3ad3 100644 --- a/core/src/main/java/edu/wpi/grip/core/events/ConnectionRemovedEvent.java +++ b/core/src/main/java/edu/wpi/grip/core/events/ConnectionRemovedEvent.java @@ -10,7 +10,7 @@ * An event that occurs when a connection is removed from the pipeline. This is triggered by the * user deleting a connection with the GUI. */ -public class ConnectionRemovedEvent { +public class ConnectionRemovedEvent implements DirtiesSaveEvent { private final Connection connection; /** diff --git a/core/src/main/java/edu/wpi/grip/core/events/DirtiesSaveEvent.java b/core/src/main/java/edu/wpi/grip/core/events/DirtiesSaveEvent.java new file mode 100644 index 0000000000..b66a591124 --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/events/DirtiesSaveEvent.java @@ -0,0 +1,19 @@ +package edu.wpi.grip.core.events; + +/** + * An event that can potentially dirty the save file. + * + *

These events ensure that anything that changes causes the save file to be flagged as dirty and + * in need of being saved for the project to be deemed "clean" again. + */ +public interface DirtiesSaveEvent { + + /** + * Some events may have more logic regarding whether they make the save dirty or not. + * + * @return True if this event should dirty the project save + */ + default boolean doesDirtySave() { + return true; + } +} diff --git a/core/src/main/java/edu/wpi/grip/core/events/SocketChangedEvent.java b/core/src/main/java/edu/wpi/grip/core/events/SocketChangedEvent.java index 6cabaf41a9..cedeb845d4 100644 --- a/core/src/main/java/edu/wpi/grip/core/events/SocketChangedEvent.java +++ b/core/src/main/java/edu/wpi/grip/core/events/SocketChangedEvent.java @@ -10,7 +10,7 @@ * An event that occurs when the value stored in a socket changes. This can happen, for example, as * the result of an operation completing, or as a response to user input. */ -public class SocketChangedEvent implements RunPipelineEvent { +public class SocketChangedEvent implements RunPipelineEvent, DirtiesSaveEvent { private final Socket socket; /** @@ -31,19 +31,26 @@ public boolean isRegarding(Socket socket) { return socket.equals(this.socket); } + /** + * This event will only dirty the save if the InputSocket does not have connections. + * Thus the value can only have been changed by a UI component. + * If the socket has connections then the value change is triggered by another socket's change. + * + * @return True if this should dirty the save. + */ + @Override + public boolean doesDirtySave() { + return pipelineShouldRun(); + } + @Override public boolean pipelineShouldRun() { /* * The pipeline should only flag an update when it is an input changing. A changed output - * doesn't mean - * the pipeline is dirty. - * If the socket is connected to another socket then then the input will only change - * because of the - * pipeline thread. - * In that case we don't want the pipeline to be releasing itself. - * If the connections are empty then the change must have come from the UI so we need to - * run the pipeline - * with the new values. + * doesn't mean the pipeline is dirty. If the socket is connected to another socket then then + * the input will only change because of the pipeline thread. In that case we don't want the + * pipeline to be releasing itself. If the connections are empty then the change must have come + * from the UI so we need to run the pipeline with the new values. */ return socket.getDirection().equals(Socket.Direction.INPUT) && socket.getConnections() .isEmpty(); diff --git a/core/src/main/java/edu/wpi/grip/core/events/SocketPreviewChangedEvent.java b/core/src/main/java/edu/wpi/grip/core/events/SocketPreviewChangedEvent.java index 4a7ab87801..ce58eb9afc 100644 --- a/core/src/main/java/edu/wpi/grip/core/events/SocketPreviewChangedEvent.java +++ b/core/src/main/java/edu/wpi/grip/core/events/SocketPreviewChangedEvent.java @@ -10,7 +10,7 @@ * An event that occurs when a {@link OutputSocket} is set to be either previewed or not previewed. * The GUI listens for these events so it knows which sockets to show previews for. */ -public class SocketPreviewChangedEvent { +public class SocketPreviewChangedEvent implements DirtiesSaveEvent { private final OutputSocket socket; /** diff --git a/core/src/main/java/edu/wpi/grip/core/events/SourceAddedEvent.java b/core/src/main/java/edu/wpi/grip/core/events/SourceAddedEvent.java index 6dde46a507..616ad970c9 100644 --- a/core/src/main/java/edu/wpi/grip/core/events/SourceAddedEvent.java +++ b/core/src/main/java/edu/wpi/grip/core/events/SourceAddedEvent.java @@ -13,7 +13,7 @@ * * @see Source */ -public class SourceAddedEvent { +public class SourceAddedEvent implements DirtiesSaveEvent { private final Source source; /** diff --git a/core/src/main/java/edu/wpi/grip/core/events/SourceRemovedEvent.java b/core/src/main/java/edu/wpi/grip/core/events/SourceRemovedEvent.java index 9b89263a65..f7f2e9cb20 100644 --- a/core/src/main/java/edu/wpi/grip/core/events/SourceRemovedEvent.java +++ b/core/src/main/java/edu/wpi/grip/core/events/SourceRemovedEvent.java @@ -13,7 +13,7 @@ * * @see Source */ -public class SourceRemovedEvent { +public class SourceRemovedEvent implements DirtiesSaveEvent { private final Source source; /** diff --git a/core/src/main/java/edu/wpi/grip/core/events/StepAddedEvent.java b/core/src/main/java/edu/wpi/grip/core/events/StepAddedEvent.java index 230b2ad3bd..5427bd056f 100644 --- a/core/src/main/java/edu/wpi/grip/core/events/StepAddedEvent.java +++ b/core/src/main/java/edu/wpi/grip/core/events/StepAddedEvent.java @@ -15,7 +15,7 @@ * An event that occurs when a new step is added to the pipeline. This is triggered by the user * adding a step with the GUI. */ -public class StepAddedEvent { +public class StepAddedEvent implements DirtiesSaveEvent { private final Step step; private final OptionalInt index; diff --git a/core/src/main/java/edu/wpi/grip/core/events/StepMovedEvent.java b/core/src/main/java/edu/wpi/grip/core/events/StepMovedEvent.java index 7dcc3ab3f9..3b9adc2786 100644 --- a/core/src/main/java/edu/wpi/grip/core/events/StepMovedEvent.java +++ b/core/src/main/java/edu/wpi/grip/core/events/StepMovedEvent.java @@ -9,7 +9,7 @@ /** * An event that occurs when a new step is moved from one position to another in the pipeline. */ -public class StepMovedEvent { +public class StepMovedEvent implements DirtiesSaveEvent { private final Step step; private final int distance; diff --git a/core/src/main/java/edu/wpi/grip/core/events/StepRemovedEvent.java b/core/src/main/java/edu/wpi/grip/core/events/StepRemovedEvent.java index 1f6b0eb1e7..005f6d5491 100644 --- a/core/src/main/java/edu/wpi/grip/core/events/StepRemovedEvent.java +++ b/core/src/main/java/edu/wpi/grip/core/events/StepRemovedEvent.java @@ -10,7 +10,7 @@ * An event that occurs when a new step is removed from the pipeline. This is triggered by the user * deleting a step from the GUI. */ -public class StepRemovedEvent { +public class StepRemovedEvent implements DirtiesSaveEvent { private final Step step; /** diff --git a/core/src/main/java/edu/wpi/grip/core/serialization/Project.java b/core/src/main/java/edu/wpi/grip/core/serialization/Project.java index e9a6416f3d..19be072e29 100644 --- a/core/src/main/java/edu/wpi/grip/core/serialization/Project.java +++ b/core/src/main/java/edu/wpi/grip/core/serialization/Project.java @@ -2,8 +2,10 @@ import edu.wpi.grip.core.Pipeline; import edu.wpi.grip.core.PipelineRunner; +import edu.wpi.grip.core.events.DirtiesSaveEvent; import com.google.common.annotations.VisibleForTesting; +import com.google.common.eventbus.Subscribe; import com.google.common.reflect.ClassPath; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.annotations.XStreamAlias; @@ -20,7 +22,6 @@ import java.io.Writer; import java.nio.charset.StandardCharsets; import java.util.Optional; - import javax.inject.Inject; import javax.inject.Singleton; @@ -36,6 +37,7 @@ public class Project { @Inject private PipelineRunner pipelineRunner; private Optional file = Optional.empty(); + private boolean saveIsDirty = false; @Inject public void initialize(StepConverter stepConverter, @@ -109,6 +111,7 @@ void open(Reader reader) { this.pipeline.clear(); this.xstream.fromXML(reader); pipelineRunner.startAsync(); + saveIsDirty = false; } /** @@ -124,5 +127,19 @@ public void save(File file) throws IOException { public void save(Writer writer) { this.xstream.toXML(this.pipeline, writer); + saveIsDirty = false; + } + + public boolean isSaveDirty() { + return saveIsDirty; + } + + @Subscribe + public void onDirtiesSaveEvent(DirtiesSaveEvent dirtySaveEvent) { + // Only update the flag the save isn't already dirty + // We don't need to be redundantly checking if the event dirties the save + if (!saveIsDirty && dirtySaveEvent.doesDirtySave()) { + saveIsDirty = true; + } } } diff --git a/ui/src/main/java/edu/wpi/grip/ui/Main.java b/ui/src/main/java/edu/wpi/grip/ui/Main.java index d9113dcc20..34129f192b 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/Main.java +++ b/ui/src/main/java/edu/wpi/grip/ui/Main.java @@ -3,6 +3,7 @@ import edu.wpi.grip.core.GripCoreModule; import edu.wpi.grip.core.GripFileModule; import edu.wpi.grip.core.PipelineRunner; +import edu.wpi.grip.core.events.DirtiesSaveEvent; import edu.wpi.grip.core.events.UnexpectedThrowableEvent; import edu.wpi.grip.core.http.GripServer; import edu.wpi.grip.core.http.HttpPipelineSwitcher; @@ -31,6 +32,7 @@ import javafx.application.Application; import javafx.application.Platform; import javafx.application.Preloader; +import javafx.beans.property.SimpleBooleanProperty; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; @@ -43,6 +45,7 @@ public class Main extends Application { private final Object dialogLock = new Object(); private static final Logger logger = Logger.getLogger(Main.class.getName()); + private static final String MAIN_TITLE = "GRIP Computer Vision Engine"; /** * JavaFX insists on creating the main application with its own reflection code, so we can't @@ -60,6 +63,7 @@ public class Main extends Application { @Inject private HttpPipelineSwitcher pipelineSwitcher; private Parent root; private boolean headless; + private final SimpleBooleanProperty dirty = new SimpleBooleanProperty(false); public static void main(String[] args) { launch(args); @@ -123,9 +127,17 @@ public void start(Stage stage) throws IOException { cvOperations.addOperations(); notifyPreloader(new Preloader.ProgressNotification(0.9)); + dirty.addListener((observable, oldValue, newValue) -> { + if (newValue) { + stage.setTitle(MAIN_TITLE + " | Edited"); + } else { + stage.setTitle(MAIN_TITLE); + } + }); + // If this isn't here this can cause a deadlock on windows. See issue #297 stage.setOnCloseRequest(event -> SafeShutdown.exit(0, Platform::exit)); - stage.setTitle("GRIP Computer Vision Engine"); + stage.setTitle(MAIN_TITLE); stage.getIcons().add(new Image("/edu/wpi/grip/ui/icons/grip.png")); stage.setScene(new Scene(root)); notifyPreloader(new Preloader.ProgressNotification(1.0)); @@ -139,6 +151,13 @@ public void stop() { SafeShutdown.flagStopping(); } + @Subscribe + public void onDirtiesSaveEvent(DirtiesSaveEvent dirtySaveEvent) { + if (dirty.get() != dirtySaveEvent.doesDirtySave()) { + dirty.set(dirtySaveEvent.doesDirtySave()); + } + } + @Subscribe @SuppressWarnings("PMD.AvoidCatchingThrowable") public final void onUnexpectedThrowableEvent(UnexpectedThrowableEvent event) { diff --git a/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java b/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java index 4850f14886..d41a01a808 100644 --- a/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java +++ b/ui/src/main/java/edu/wpi/grip/ui/MainWindowController.java @@ -93,7 +93,6 @@ protected void initialize() { .toString()); statusBar.setText(" Pipeline " + stateMessage); }), Platform::runLater); - } /** @@ -103,7 +102,7 @@ protected void initialize() { * @return true If the user has not chosen to */ private boolean showConfirmationDialogAndWait() { - if (!pipeline.getSteps().isEmpty()) { + if (!pipeline.getSteps().isEmpty() && project.isSaveDirty()) { final ButtonType save = new ButtonType("Save"); final ButtonType dontSave = ButtonType.NO; final ButtonType cancel = ButtonType.CANCEL;