diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 797f2ae0..2b3a8a10 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,8 +16,9 @@ jobs: submodules: recursive - name: Set up JDK 11 - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: + distribution: 'adopt' java-version: 11 java-package: jdk - name: Cache Gradle packages diff --git a/.gitignore b/.gitignore index 938ad38a..977328e8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,10 @@ lib/*.dll .idea/ /out/ +#VsCode +/bin/ +.vscode/ + ### Gradle ### .gradle /build/ diff --git a/.gitmodules b/.gitmodules index 217c921a..808fd3cf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "src/main/proto"] path = src/main/proto - url = https://github.com/Ecdar/Ecdar-ProtoBuf.git + url = https://github.com/Ecdar-SW5/Ecdar-ProtoBuf.git + diff --git a/build.gradle b/build.gradle index 1b049e61..c7664e51 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,20 @@ javafx { modules = ['javafx.controls', 'javafx.fxml', 'javafx.swing'] } +run { + mainClassName = 'com.jfxbase/com.jfxbase.sample.Main' + applicationDefaultJvmArgs += [ + "--add-opens", "javafx.graphics/javafx.css=ALL-UNNAMED", + "--add-opens", "javafx.base/com.sun.javafx.runtime=ALL-UNNAMED", + "--add-opens", "javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED", + "--add-opens", "javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED", + "--add-opens", "javafx.base/com.sun.javafx.binding=ALL-UNNAMED", + "--add-opens", "javafx.base/com.sun.javafx.event=ALL-UNNAMED", + "--add-opens", "javafx.graphics/com.sun.javafx.stage=ALL-UNNAMED", + "--add-opens", "javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED", + ] +} + group 'ecdar' if (project.hasProperty('ecdarVersion')) { version = project.ecdarVersion @@ -46,7 +60,6 @@ def protocVersion = protobufVersion dependencies { implementation fileTree(dir: 'lib', include: ['*.jar']) - implementation 'com.jfoenix:jfoenix:9.0.10' implementation group: 'de.codecentric.centerdevice', name: 'javafxsvg', version: '1.3.0' implementation 'org.kordamp.ikonli:ikonli-core:12.3.1' @@ -79,6 +92,7 @@ dependencies { testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.8.2' testImplementation group: 'org.testfx', name: 'testfx-junit', version: '4.0.15-alpha' testImplementation group: 'org.testfx', name: 'openjfx-monocle', version: 'jdk-12.0.1+2' + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: '5.8.2' } diff --git a/src/main/java/ecdar/Ecdar.java b/src/main/java/ecdar/Ecdar.java index 05bd9efc..01775eac 100644 --- a/src/main/java/ecdar/Ecdar.java +++ b/src/main/java/ecdar/Ecdar.java @@ -4,6 +4,8 @@ import ecdar.abstractions.Project; import ecdar.backend.BackendDriver; import ecdar.backend.BackendHelper; +import ecdar.backend.QueryHandler; +import ecdar.backend.SimulationHandler; import ecdar.code_analysis.CodeAnalysis; import ecdar.controllers.EcdarController; import ecdar.presentations.BackgroundThreadPresentation; @@ -52,6 +54,8 @@ public class Ecdar extends Application { public static BooleanProperty shouldRunBackgroundQueries = new SimpleBooleanProperty(true); private static final BooleanProperty isSplit = new SimpleBooleanProperty(true); //Set to true to ensure correct behaviour at first toggle. private static BackendDriver backendDriver = new BackendDriver(); + private static QueryHandler queryHandler = new QueryHandler(backendDriver); + private static SimulationHandler simulationHandler; private Stage debugStage; /** @@ -120,6 +124,20 @@ public static Project getProject() { return project; } + /** + * Returns the backend driver used to execute queries and handle simulation + * @return BackendDriver + */ + public static BackendDriver getBackendDriver() { + return backendDriver; + } + + public static QueryHandler getQueryExecutor() { + return queryHandler; + } + + public static SimulationHandler getSimulationHandler() { return simulationHandler; } + public static EcdarPresentation getPresentation() { return presentation; } @@ -134,8 +152,8 @@ public static void showHelp() { presentation.showHelp(); } - public static BooleanProperty toggleFilePane() { - return presentation.toggleFilePane(); + public static BooleanProperty toggleLeftPane() { + return presentation.toggleLeftPane(); } /** @@ -160,7 +178,7 @@ public static BooleanProperty toggleRunBackgroundQueries() { } public static BooleanProperty toggleQueryPane() { - return presentation.toggleQueryPane(); + return presentation.toggleRightPane(); } /** @@ -181,14 +199,6 @@ public static BooleanProperty toggleCanvasSplit() { return isSplit; } - /** - * Returns the backend driver used to execute queries and handle simulation - * @return BackendDriver - */ - public static BackendDriver getBackendDriver() { - return backendDriver; - } - public static double getDpiScale() { if (!autoScalingEnabled.getValue()) return 1; @@ -207,6 +217,7 @@ public void start(final Stage stage) { // Load or create new project project = new Project(); + simulationHandler = new SimulationHandler(getBackendDriver()); // Set the title for the application stage.setTitle("Ecdar " + VERSION); @@ -306,6 +317,8 @@ public void start(final Stage stage) { try { backendDriver.closeAllBackendConnections(); + queryHandler.closeAllBackendConnections(); + simulationHandler.closeAllBackendConnections(); } catch (IOException e) { e.printStackTrace(); } @@ -319,6 +332,8 @@ public void start(final Stage stage) { // to prevent dangling connections and queries try { backendDriver.closeAllBackendConnections(); + queryHandler.closeAllBackendConnections(); + simulationHandler.closeAllBackendConnections(); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/main/java/ecdar/abstractions/Component.java b/src/main/java/ecdar/abstractions/Component.java index d3da9db1..37789bfe 100644 --- a/src/main/java/ecdar/abstractions/Component.java +++ b/src/main/java/ecdar/abstractions/Component.java @@ -38,7 +38,9 @@ public class Component extends HighLevelModelObject implements Boxed { // Verification properties private final ObservableList locations = FXCollections.observableArrayList(); + private final ObservableList failingLocations = FXCollections.observableArrayList(); private final ObservableList edges = FXCollections.observableArrayList(); + private final ObservableList failingEdges = FXCollections.observableArrayList(); private final ObservableList inputStrings = FXCollections.observableArrayList(); private final ObservableList outputStrings = FXCollections.observableArrayList(); private final StringProperty description = new SimpleStringProperty(""); @@ -486,6 +488,67 @@ public boolean removeLocation(final Location location) { return locations.remove(location); } + /** + * Add a failing Edge to the list of failing edges + * and set the edge's failing property to true. + * @param edge the Edge that is failing. + * @return whether adding the Edge to the list was a success + */ + public boolean addFailingEdge(final Edge edge) { + edge.setFailing(true); + return failingEdges.add(edge); + } + + /** + * Sets all previous failing edges to not failing + * and removes all previous failing edges from list. + */ + public void removeFailingEdges() { + for (DisplayableEdge edge : failingEdges) { + edge.setFailing(false); + } + failingEdges.removeAll(); + } + + /** + * Observable list of all failing locations. + * @return Observable list of all failing locations. + */ + public ObservableList getFailingLocations() { + return failingLocations; + } + + /** + * Adds a failing location to the list of failing locations. + * @param locationId the id of the location that is failing. + * @return whether adding the location to the list was a success + */ + public boolean addFailingLocation(final String locationId) { + Location failingLocation = findLocation(locationId); + failingLocation.setFailing(true); + return failingLocations.add(failingLocation); + } + + /** + * Sets all previous failing locations to not failing + * and removes all previous failing locations from list. + */ + public void removeFailingLocations() { + for (Location location : failingLocations) { + location.setFailing(false); + } + failingLocations.removeAll(); + } + + /** + * Observable list of all failing edges. + * @return Observable list of all failing edges. + */ + public ObservableList getFailingEdges() { + return failingEdges; + } + + /** * Returns all DisplayableEdges of the component (returning a list potentially containing GroupEdges and Edges) * @return All visual edges of the component diff --git a/src/main/java/ecdar/abstractions/ComponentInstance.java b/src/main/java/ecdar/abstractions/ComponentInstance.java index 391cae3d..260b44ce 100644 --- a/src/main/java/ecdar/abstractions/ComponentInstance.java +++ b/src/main/java/ecdar/abstractions/ComponentInstance.java @@ -2,7 +2,6 @@ import ecdar.Ecdar; import ecdar.presentations.Grid; -import ecdar.utility.colors.Color; import com.google.gson.JsonObject; import javafx.beans.property.*; import javafx.beans.value.ObservableValue; diff --git a/src/main/java/ecdar/abstractions/Declarations.java b/src/main/java/ecdar/abstractions/Declarations.java index 38f179cb..2014d729 100644 --- a/src/main/java/ecdar/abstractions/Declarations.java +++ b/src/main/java/ecdar/abstractions/Declarations.java @@ -1,15 +1,12 @@ package ecdar.abstractions; import ecdar.utility.colors.Color; -import ecdar.utility.colors.EnabledColor; -import com.google.gson.JsonArray; import com.google.gson.JsonObject; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import org.apache.commons.lang3.tuple.Triple; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/src/main/java/ecdar/abstractions/DisplayableEdge.java b/src/main/java/ecdar/abstractions/DisplayableEdge.java index 7a5d62d2..be812729 100644 --- a/src/main/java/ecdar/abstractions/DisplayableEdge.java +++ b/src/main/java/ecdar/abstractions/DisplayableEdge.java @@ -1,16 +1,17 @@ package ecdar.abstractions; -import ecdar.Ecdar; import ecdar.code_analysis.Nearable; import ecdar.presentations.Grid; import ecdar.utility.colors.Color; import ecdar.utility.helpers.Circular; import ecdar.utility.helpers.MouseCircular; +import ecdar.utility.helpers.StringHelper; import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import java.util.List; +import java.util.UUID; public abstract class DisplayableEdge implements Nearable { private final StringProperty id = new SimpleStringProperty(""); @@ -39,6 +40,8 @@ public abstract class DisplayableEdge implements Nearable { private final BooleanProperty isHighlighted = new SimpleBooleanProperty(false); + protected final BooleanProperty failing = new SimpleBooleanProperty(false); + public Location getSourceLocation() { return sourceLocation.get(); } @@ -82,7 +85,7 @@ public StringProperty selectProperty() { } public String getGuard() { - return guard.get(); + return StringHelper.ConvertUnicodeToSymbols(guard.get()); } public void setGuard(final String guard) { @@ -279,12 +282,7 @@ public String getId() { * Generate and sets a unique id for this location */ protected void setId() { - for(int counter = 0; ; counter++) { - if(!Ecdar.getProject().getEdgeIds().contains(String.valueOf(counter))){ - id.set(Edge.EDGE + counter); - return; - } - } + id.set(UUID.randomUUID().toString()); } /** @@ -302,4 +300,22 @@ public StringProperty idProperty() { public abstract List getProperty(final PropertyType propertyType); public abstract void setProperty(final PropertyType propertyType, final List newProperty); + + /** + * Sets the 'failing' property + * @param bool true if the edge is failing. + */ + public abstract void setFailing(final boolean bool); + + /** + * Getter for the 'failing' boolean + * @return Whether edge is failing in last query response. + */ + public abstract boolean getFailing(); + + /** + * The observable boolean property for 'failing' of this. + * @return The observable boolean property for 'failing' of this. + */ + public abstract BooleanProperty failingProperty(); } diff --git a/src/main/java/ecdar/abstractions/EcdarSystemEdge.java b/src/main/java/ecdar/abstractions/EcdarSystemEdge.java index d9d30f2c..503e4831 100644 --- a/src/main/java/ecdar/abstractions/EcdarSystemEdge.java +++ b/src/main/java/ecdar/abstractions/EcdarSystemEdge.java @@ -2,7 +2,6 @@ import ecdar.Ecdar; import ecdar.controllers.SystemRootController; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; diff --git a/src/main/java/ecdar/abstractions/Edge.java b/src/main/java/ecdar/abstractions/Edge.java index 995e2ce8..22c5cdbe 100644 --- a/src/main/java/ecdar/abstractions/Edge.java +++ b/src/main/java/ecdar/abstractions/Edge.java @@ -68,7 +68,7 @@ public String getSyncWithSymbol() { return sync.get() + (ioStatus.get().equals(EdgeStatus.INPUT) ? "?" : "!"); } - public String getGroup(){ + public String getGroup() { return group.get(); } @@ -76,6 +76,21 @@ public void setGroup(final String string){ group.set(string); } + @Override + public boolean getFailing() { + return this.failing.get(); + } + + @Override + public void setFailing(boolean bool) { + this.failing.set(bool); + } + + @Override + public BooleanProperty failingProperty() { + return this.failing; + } + /** * Creates a clone of an edge. * Clones objects used for verification. @@ -192,10 +207,17 @@ public void deserialize(final JsonObject json, final Component component) { } ioStatus = new SimpleObjectProperty<>(EdgeStatus.valueOf(json.getAsJsonPrimitive(STATUS).getAsString())); - final JsonPrimitive IDJson = json.getAsJsonPrimitive(ID); - if (IDJson != null) setId(IDJson.getAsString()); - else setId(); + /* The new type of edge ids is a UUID, which has a length of 36 characters. + * The if statement checks if the id is a UUID and if not, it creates a new UUID. + * This is necessary because the old type of edge ids were not unique and has to be replaced in old projects. + * It should be possible to simplify this in the future when all projects are updated. + */ + int UUIDLength = 36; + if (IDJson != null && IDJson.getAsString().length() == UUIDLength) + setId(IDJson.getAsString()); + else + setId(); final JsonPrimitive groupJson = json.getAsJsonPrimitive(GROUP); diff --git a/src/main/java/ecdar/abstractions/GroupedEdge.java b/src/main/java/ecdar/abstractions/GroupedEdge.java index 6f216efc..02503d9a 100644 --- a/src/main/java/ecdar/abstractions/GroupedEdge.java +++ b/src/main/java/ecdar/abstractions/GroupedEdge.java @@ -1,6 +1,5 @@ package ecdar.abstractions; -import ecdar.Ecdar; import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; @@ -8,6 +7,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.UUID; public class GroupedEdge extends DisplayableEdge { private final ObservableList edges = FXCollections.observableList(new ArrayList<>()); @@ -90,12 +90,7 @@ public String getId() { * Generate and sets a unique id for this location */ protected void setId() { - for(int counter = 0; ; counter++) { - if(!Ecdar.getProject().getEdgeIds().contains(String.valueOf(counter))){ - id.set(Edge.EDGE_GROUP + counter); - return; - } - } + id.set(UUID.randomUUID().toString()); } /** @@ -168,6 +163,21 @@ public void setProperty(final PropertyType propertyType, final List newP } } + @Override + public void setFailing(boolean bool) { + this.failing.set(bool); + } + + @Override + public boolean getFailing() { + return this.failing.get(); + } + + @Override + public BooleanProperty failingProperty() { + return this.failing; + } + public List getSyncProperties() { List syncProperties = new ArrayList<>(); diff --git a/src/main/java/ecdar/abstractions/Location.java b/src/main/java/ecdar/abstractions/Location.java index b490f5b9..e75e4797 100644 --- a/src/main/java/ecdar/abstractions/Location.java +++ b/src/main/java/ecdar/abstractions/Location.java @@ -8,6 +8,7 @@ import ecdar.utility.colors.Color; import ecdar.utility.colors.EnabledColor; import ecdar.utility.helpers.Circular; +import ecdar.utility.helpers.StringHelper; import ecdar.utility.serialize.Serializable; import com.google.common.base.Strings; import com.google.gson.Gson; @@ -57,6 +58,7 @@ public class Location implements Circular, Serializable, Nearable, DropDownMenu. private final ObjectProperty reachability = new SimpleObjectProperty<>(); private final SimpleBooleanProperty isLocked = new SimpleBooleanProperty(false); + private final BooleanProperty failing = new SimpleBooleanProperty(false); public Location() { } @@ -87,6 +89,22 @@ public Location(final JsonObject jsonObject) { bindReachabilityAnalysis(); } + // ToDo NIELS: Comment in, when location should be received through ProtoBuf +// public Location(ComponentProtos.Location protoBufLocation) { +// setId(protoBufLocation.getId()); +// setNickname(protoBufLocation.getNickname()); +// setInvariant(protoBufLocation.getInvariant()); +// setType(Type.valueOf(protoBufLocation.getType())); +// setUrgency(Urgency.valueOf(protoBufLocation.getUrgency())); +// setX(protoBufLocation.getX()); +// setY(protoBufLocation.getY()); +// setColor(Color.valueOf(protoBufLocation.getColor())); +// setNicknameX(protoBufLocation.getNicknameX()); +// setNicknameY(protoBufLocation.getNicknameY()); +// setInvariantX(protoBufLocation.getInvariantX()); +// setInvariantY(protoBufLocation.getInvariantY()); +// } + /** * Generates an id for this, and binds reachability analysis. */ @@ -161,7 +179,7 @@ public StringProperty idProperty() { } public String getInvariant() { - return invariant.get(); + return StringHelper.ConvertUnicodeToSymbols(invariant.get()); } public void setInvariant(final String invariant) { @@ -451,6 +469,31 @@ public void deserialize(final JsonObject json) { public String generateNearString() { return "Location " + (!Strings.isNullOrEmpty(getNickname()) ? (getNickname() + " (" + getId() + ")") : getId()); } + + /** + * Sets whether this location failed for the last query + * @param bool if a query responded failure with the location, bool should be true. + */ + public void setFailing(boolean bool) { + this.failing.set(bool); + } + + /** + * Whether this location is currently failing. + * @return Whether this location is currently failing. + */ + public boolean getFailing() { + return this.failing.get(); + } + + /** + * The observable boolean property for 'failing' of this. + * @return The observable boolean property for 'failing' of this. + */ + public BooleanProperty failingProperty() { + return this.failing; + } + public enum Type { NORMAL, INITIAL, UNIVERSAL, INCONSISTENT } diff --git a/src/main/java/ecdar/abstractions/Nail.java b/src/main/java/ecdar/abstractions/Nail.java index 5ed84d45..75375d19 100644 --- a/src/main/java/ecdar/abstractions/Nail.java +++ b/src/main/java/ecdar/abstractions/Nail.java @@ -39,6 +39,13 @@ public Nail(final JsonObject jsonObject) { deserialize(jsonObject); } + // ToDo NIELS: Comment in, when location should be received through ProtoBuf +// public Nail(ComponentProtos.Nail protoBufNail) { +// setPropertyType(DisplayableEdge.PropertyType.valueOf(protoBufNail.getPropertyType())); +// setPropertyX(protoBufNail.getPropertyX()); +// setPropertyY(protoBufNail.getPropertyY()); +// } + public double getX() { return x.get(); } diff --git a/src/main/java/ecdar/abstractions/Query.java b/src/main/java/ecdar/abstractions/Query.java index 2fd0b7f3..76cc5930 100644 --- a/src/main/java/ecdar/abstractions/Query.java +++ b/src/main/java/ecdar/abstractions/Query.java @@ -1,50 +1,100 @@ package ecdar.abstractions; +import EcdarProtoBuf.ObjectProtos; import ecdar.Ecdar; import ecdar.backend.*; import ecdar.controllers.EcdarController; -import ecdar.utility.helpers.StringValidator; +import ecdar.utility.helpers.StringHelper; import ecdar.utility.serialize.Serializable; import com.google.gson.JsonObject; import javafx.application.Platform; import javafx.beans.property.*; -import java.util.HashMap; -import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; public class Query implements Serializable { private static final String QUERY = "query"; private static final String COMMENT = "comment"; private static final String IS_PERIODIC = "isPeriodic"; - private static final String IGNORED_INPUTS = "ignoredInputs"; - private static final String IGNORED_OUTPUTS = "ignoredOutputs"; private static final String BACKEND = "backend"; - public final HashMap ignoredInputs = new HashMap<>(); - public final HashMap ignoredOutputs = new HashMap<>(); - - private final ObjectProperty queryState = new SimpleObjectProperty<>(QueryState.UNKNOWN); private final StringProperty query = new SimpleStringProperty(""); private final StringProperty comment = new SimpleStringProperty(""); - private final SimpleBooleanProperty isPeriodic = new SimpleBooleanProperty(false); private final StringProperty errors = new SimpleStringProperty(""); + private final SimpleBooleanProperty isPeriodic = new SimpleBooleanProperty(false); + private final ObjectProperty queryState = new SimpleObjectProperty<>(QueryState.UNKNOWN); private final ObjectProperty type = new SimpleObjectProperty<>(); private BackendInstance backend; - private Runnable runQuery; + + + private final Consumer successConsumer = (aBoolean) -> { + if (aBoolean) { + for (Component c : Ecdar.getProject().getComponents()) { + c.removeFailingLocations(); + c.removeFailingEdges(); + } + setQueryState(QueryState.SUCCESSFUL); + } else { + setQueryState(QueryState.ERROR); + } + }; + + private Boolean forcedCancel = false; + private final Consumer failureConsumer = (e) -> { + if (forcedCancel) { + setQueryState(QueryState.UNKNOWN); + } else { + setQueryState(QueryState.SYNTAX_ERROR); + if (e instanceof BackendException.MissingFileQueryException) { + Ecdar.showToast("Please save the project before trying to run queries"); + } + + this.addError(e.getMessage()); + final Throwable cause = e.getCause(); + if (cause != null) { + // We had trouble generating the model if we get a NullPointerException + if (cause instanceof NullPointerException) { + setQueryState(QueryState.UNKNOWN); + } else { + Platform.runLater(() -> EcdarController.openQueryDialog(this, cause.toString())); + } + } + } + }; + + private final BiConsumer stateActionConsumer = (state, action) -> { + for (Component c : Ecdar.getProject().getComponents()) { + c.removeFailingLocations(); + c.removeFailingEdges(); + } + for (ObjectProtos.Location location : state.getLocationTuple().getLocationsList()) { + Component c = Ecdar.getProject().findComponent(location.getSpecificComponent().getComponentName()); + if (c == null) { + throw new NullPointerException("Could not find the specific component: " + location.getSpecificComponent().getComponentName()); + } + Location l = c.findLocation(location.getId()); + if (l == null) { + throw new NullPointerException("Could not find location: " + location.getId()); + } + c.addFailingLocation(l.getId()); + for (Edge edge : c.getEdges()) { + if(action.equals(edge.getSync()) && edge.getSourceLocation() == l) { + c.addFailingEdge(edge); + } + } + } + }; public Query(final String query, final String comment, final QueryState queryState) { - this.query.set(query); + this.setQuery(query); this.comment.set(comment); this.queryState.set(queryState); setBackend(BackendHelper.getDefaultBackendInstance()); - - initializeRunQuery(); } public Query(final JsonObject jsonElement) { deserialize(jsonElement); - - initializeRunQuery(); } public QueryState getQueryState() { @@ -60,7 +110,7 @@ public ObjectProperty queryStateProperty() { } public String getQuery() { - return query.get(); + return StringHelper.ConvertUnicodeToSymbols(this.query.get()); } public void setQuery(final String query) { @@ -117,55 +167,23 @@ public ObjectProperty getTypeProperty() { return this.type; } - private Boolean forcedCancel = false; + public Consumer getSuccessConsumer() { + return successConsumer; + } - private void initializeRunQuery() { - runQuery = () -> { - setQueryState(QueryState.RUNNING); - forcedCancel = false; - errors.set(""); - - if (getQuery().isEmpty()) { - setQueryState(QueryState.SYNTAX_ERROR); - this.addError("Query is empty"); - return; - } - - Ecdar.getBackendDriver().addQueryToExecutionQueue(getType().getQueryName() + ": " + getQuery() + " " + getIgnoredInputOutputsOnQuery(), - getBackend(), - aBoolean -> { - if (aBoolean) { - setQueryState(QueryState.SUCCESSFUL); - } else { - setQueryState(QueryState.ERROR); - } - }, - e -> { - if (forcedCancel) { - setQueryState(QueryState.UNKNOWN); - } else { - setQueryState(QueryState.SYNTAX_ERROR); - if (e instanceof BackendException.MissingFileQueryException) { - Ecdar.showToast("Please save the project before trying to run queries"); - } - - this.addError(e.getMessage()); - final Throwable cause = e.getCause(); - if (cause != null) { - // We had trouble generating the model if we get a NullPointerException - if (cause instanceof NullPointerException) { - setQueryState(QueryState.UNKNOWN); - } else { - Platform.runLater(() -> EcdarController.openQueryDialog(this, cause.toString())); - } - } - } - }, - new QueryListener(this) - ); - }; + public Consumer getFailureConsumer() { + return failureConsumer; } + /** + * Getter for the state action consumer. + * @return The State Consumer + */ + public BiConsumer getStateActionConsumer() { + return stateActionConsumer; + } + + @Override public JsonObject serialize() { final JsonObject result = new JsonObject(); @@ -173,23 +191,11 @@ public JsonObject serialize() { result.addProperty(QUERY, getType().getQueryName() + ": " + getQuery()); result.addProperty(COMMENT, getComment()); result.addProperty(IS_PERIODIC, isPeriodic()); - - result.add(IGNORED_INPUTS, getHashMapAsJsonObject(ignoredInputs)); - result.add(IGNORED_OUTPUTS, getHashMapAsJsonObject(ignoredOutputs)); - result.addProperty(BACKEND, backend.getName()); return result; } - private JsonObject getHashMapAsJsonObject(HashMap ignoredOutputs) { - JsonObject resultingJsonObject = new JsonObject(); - for (Map.Entry currentPair : ignoredOutputs.entrySet()) { - resultingJsonObject.addProperty(currentPair.getKey(), currentPair.getValue()); - } - return resultingJsonObject; - } - @Override public void deserialize(final JsonObject json) { String query = json.getAsJsonPrimitive(QUERY).getAsString(); @@ -208,14 +214,6 @@ public void deserialize(final JsonObject json) { setIsPeriodic(json.getAsJsonPrimitive(IS_PERIODIC).getAsBoolean()); } - if (json.has(IGNORED_INPUTS)) { - deserializeJsonObjectToMap(json.getAsJsonObject(IGNORED_INPUTS), ignoredInputs); - } - - if (json.has(IGNORED_OUTPUTS)) { - deserializeJsonObjectToMap(json.getAsJsonObject(IGNORED_OUTPUTS), ignoredOutputs); - } - if(json.has(BACKEND)) { setBackend(BackendHelper.getBackendInstanceByName(json.getAsJsonPrimitive(BACKEND).getAsString())); } else { @@ -223,14 +221,6 @@ public void deserialize(final JsonObject json) { } } - private void deserializeJsonObjectToMap(JsonObject jsonObject, HashMap associatedMap) { - jsonObject.entrySet().forEach((entry) -> associatedMap.put(entry.getKey(), entry.getValue().getAsBoolean())); - } - - public void run() { - if (StringValidator.validateQuery(query.get())) runQuery.run(); - } - public void cancel() { if (getQueryState().equals(QueryState.RUNNING)) { forcedCancel = true; @@ -245,40 +235,4 @@ public void addError(String e) { public String getCurrentErrors() { return errors.getValue(); } - - private String getIgnoredInputOutputsOnQuery() { - if (!BackendHelper.backendSupportsInputOutputs(this.backend) || (!ignoredInputs.containsValue(true) && !ignoredOutputs.containsValue(true))) { - return ""; - } - - // Create StringBuilder starting with a quotation mark to signal start of extra outputs - StringBuilder ignoredInputOutputsStringBuilder = new StringBuilder("--ignored_outputs=\""); - - // Append outputs, comma separated - appendMapItemsWithValueTrue(ignoredInputOutputsStringBuilder, ignoredOutputs); - - // Append quotation marks to signal end of outputs and start of inputs - ignoredInputOutputsStringBuilder.append("\" --ignored_inputs=\""); - - // Append inputs, comma separated - appendMapItemsWithValueTrue(ignoredInputOutputsStringBuilder, ignoredInputs); - - // Append quotation mark to signal end of extra inputs - ignoredInputOutputsStringBuilder.append("\""); - - return ignoredInputOutputsStringBuilder.toString(); - } - - private void appendMapItemsWithValueTrue(StringBuilder stringBuilder, Map map) { - map.forEach((key, value) -> { - if (value) { - stringBuilder.append(key); - stringBuilder.append(","); - } - }); - - if (stringBuilder.lastIndexOf(",") + 1 == stringBuilder.length()) { - stringBuilder.setLength(stringBuilder.length() - 1); - } - } } diff --git a/src/main/java/ecdar/abstractions/QueryType.java b/src/main/java/ecdar/abstractions/QueryType.java index 98af6fe1..0ea4b1f2 100644 --- a/src/main/java/ecdar/abstractions/QueryType.java +++ b/src/main/java/ecdar/abstractions/QueryType.java @@ -2,7 +2,7 @@ public enum QueryType { REACHABILITY("reachability", "E<>"), - REFINEMENT("refinement", "<="), + REFINEMENT("refinement", "\u2264"), QUOTIENT("quotient", "\\"), SPECIFICATION("specification", "Spec"), IMPLEMENTATION("implementation", "Imp"), diff --git a/src/main/java/ecdar/abstractions/Transition.java b/src/main/java/ecdar/abstractions/Transition.java new file mode 100644 index 00000000..c79d4f21 --- /dev/null +++ b/src/main/java/ecdar/abstractions/Transition.java @@ -0,0 +1,12 @@ +package ecdar.abstractions; + +import EcdarProtoBuf.ObjectProtos; +import java.util.ArrayList; + +public class Transition { + public final ArrayList edges = new ArrayList<>(); + + public Transition(ObjectProtos protoBufTransition) { + // ToDo: Construct transition instance based on protoBuf input + } +} diff --git a/src/main/java/ecdar/backend/BackendConnection.java b/src/main/java/ecdar/backend/BackendConnection.java new file mode 100644 index 00000000..dd81945e --- /dev/null +++ b/src/main/java/ecdar/backend/BackendConnection.java @@ -0,0 +1,65 @@ +package ecdar.backend; + +import EcdarProtoBuf.EcdarBackendGrpc; +import ecdar.abstractions.BackendInstance; +import io.grpc.ManagedChannel; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +public class BackendConnection { + private final Process process; + private final EcdarBackendGrpc.EcdarBackendStub stub; + private final ManagedChannel channel; + private final BackendInstance backendInstance; + + BackendConnection(BackendInstance backendInstance, Process process, EcdarBackendGrpc.EcdarBackendStub stub, ManagedChannel channel) { + this.process = process; + this.backendInstance = backendInstance; + this.stub = stub; + this.channel = channel; + } + + /** + * Get the gRPC stub of the connection to use for query execution + * + * @return the gRPC stub of this connection + */ + public EcdarBackendGrpc.EcdarBackendStub getStub() { + return stub; + } + + /** + * Get the backend instance that should be used to execute + * the query currently associated with this backend connection + * + * @return the instance of the associated executable query object, + * or null, if no executable query is currently associated + */ + public BackendInstance getBackendInstance() { + return backendInstance; + } + + /** + * Close the gRPC connection and end the process + * + * @throws IOException originally thrown by the destroy method on java.lang.Process + */ + public void close() throws IOException { + if (!channel.isShutdown()) { + try { + channel.shutdown(); + if (!channel.awaitTermination(45, TimeUnit.SECONDS)) { + channel.shutdownNow(); // Forcefully close the connection + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + // If the backend-instance is remote, there will not be a process + if (process != null) { + process.destroy(); + } + } +} diff --git a/src/main/java/ecdar/backend/BackendDriver.java b/src/main/java/ecdar/backend/BackendDriver.java index 8d6c7c09..3fe70c96 100644 --- a/src/main/java/ecdar/backend/BackendDriver.java +++ b/src/main/java/ecdar/backend/BackendDriver.java @@ -1,21 +1,9 @@ package ecdar.backend; -import EcdarProtoBuf.ComponentProtos; import EcdarProtoBuf.EcdarBackendGrpc; -import EcdarProtoBuf.QueryProtos; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.protobuf.Empty; import ecdar.Ecdar; import ecdar.abstractions.BackendInstance; -import ecdar.abstractions.Component; -import ecdar.abstractions.QueryState; -import ecdar.controllers.EcdarController; -import ecdar.utility.UndoRedoStack; import io.grpc.*; -import io.grpc.stub.StreamObserver; -import javafx.application.Platform; -import javafx.collections.ObservableList; import org.springframework.util.SocketUtils; import java.io.*; @@ -23,198 +11,37 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; public class BackendDriver { - private final BlockingQueue queryQueue = new ArrayBlockingQueue<>(200); - private final Map> openBackendConnections = new HashMap<>(); - private final int deadlineForResponses = 20000; - private final int rerunQueryDelay = 200; + private final BlockingQueue requestQueue = new ArrayBlockingQueue<>(200); + private final Map> openBackendConnections = new HashMap<>(); // ToDo NIELS: Remove and close when backend is no longer needed + private final int responseDeadline = 20000; + private final int rerunRequestDelay = 200; private final int numberOfRetriesPerQuery = 5; public BackendDriver() { // ToDo NIELS: Consider multiple consumer threads using 'for(int i = 0; i < x; i++) {}' - QueryConsumer consumer = new QueryConsumer(queryQueue); + GrpcRequestConsumer consumer = new GrpcRequestConsumer(); Thread consumerThread = new Thread(consumer); consumerThread.start(); } - /** - * Enqueue query for execution with consumers for success and failure, which are executed when a response or - * an error is received from the backend - * - * @param query the query to be executed - * @param backendInstance name of the backend to execute the query with - * @param success consumer for a successful response - * @param failure consumer for a failure response - * @param queryListener query listener for referencing the query for GUI purposes - */ - public void addQueryToExecutionQueue(String query, - BackendInstance backendInstance, - Consumer success, - Consumer failure, - QueryListener queryListener) { - queryQueue.add(new ExecutableQuery(query, backendInstance, success, failure, queryListener)); - } - - /** - * ToDo NIELS: Reimplement this with query queue when backends support this feature - * Asynchronous method for fetching inputs and outputs for the given refinement query and adding these to the - * ignored inputs and outputs pane for the given query. - * - * @param query the ignored input output query containing the query and related GUI elements - * @param backendInstance the backend that should be used to execute the query - */ - public void getInputOutputs(IgnoredInputOutputQuery query, BackendInstance backendInstance) { - final BackendConnection backendConnection; - try { - backendConnection = getBackendConnection(backendInstance); - } catch (BackendException.NoAvailableBackendConnectionException e) { - if (query.tries < numberOfRetriesPerQuery) { - new Timer().schedule(new TimerTask() { - @Override - public void run() { - getInputOutputs(query, backendInstance); - } - }, rerunQueryDelay); - } - - return; - } - - QueryProtos.ComponentsUpdateRequest.Builder componentsBuilder = QueryProtos.ComponentsUpdateRequest.newBuilder(); - for (Component c : Ecdar.getProject().getComponents()) { - componentsBuilder.addComponents(ComponentProtos.Component.newBuilder().setJson(c.serialize().toString()).build()); - } - - executeGrpcRequest(query.getQuery().getQuery(), - backendConnection, - componentsBuilder, - QueryProtos.IgnoredInputOutputs.newBuilder().getDefaultInstanceForType(), - (reponse) -> { - if (reponse.hasQuery() && reponse.getQuery().hasIgnoredInputOutputs()) { - var ignoredInputOutputs = reponse.getQuery().getIgnoredInputOutputs(); - query.addNewElementsToMap(new ArrayList<>(ignoredInputOutputs.getIgnoredInputsList()), new ArrayList<>(ignoredInputOutputs.getIgnoredOutputsList())); - } else { - // Response is unexpected, maybe just ignore - } - }, (t) -> { - } - ); - + public int getResponseDeadline() { + return responseDeadline; } /** - * Close all open backend connection and kill all locally running processes + * Add a GrpcRequest to the request queue to be executed when a backend is available * - * @throws IOException if any of the sockets do not respond + * @param request The GrpcRequest to be executed later */ - public void closeAllBackendConnections() throws IOException { - for (BlockingQueue bq : openBackendConnections.values()) { - for (BackendConnection bc : bq) bc.close(); - } + public void addRequestToExecutionQueue(GrpcRequest request) { + requestQueue.add(request); } - private void executeQuery(ExecutableQuery executableQuery, BackendConnection backendConnection) { - if (executableQuery.queryListener.getQuery().getQueryState() == QueryState.UNKNOWN) return; - - QueryProtos.ComponentsUpdateRequest.Builder componentsBuilder = QueryProtos.ComponentsUpdateRequest.newBuilder(); - for (Component c : Ecdar.getProject().getComponents()) { - componentsBuilder.addComponents(ComponentProtos.Component.newBuilder().setJson(c.serialize().toString()).build()); - } - - executeGrpcRequest(executableQuery, backendConnection, componentsBuilder); - } - - /** - * Executes the specified query as a gRPC request using the specified backend connection. - * componentsBuilder is used to update the components of the engine. - * - * @param executableQuery executable query to be executed by the backend - * @param backendConnection connection to the backend - * @param componentsBuilder components builder containing the components relevant to the query execution - */ - private void executeGrpcRequest(ExecutableQuery executableQuery, - BackendConnection backendConnection, - QueryProtos.ComponentsUpdateRequest.Builder componentsBuilder) { - executeGrpcRequest(executableQuery.query, - backendConnection, - componentsBuilder, - null, - (response) -> handleQueryResponse(response, executableQuery), - (error) -> handleQueryBackendError(error, executableQuery) - ); - } - - /** - * Executes the specified query as a gRPC request using the specified backend connection. - * componentsBuilder is used to update the components of the engine and on completion of this transaction, - * the query is sent and its response is consumed by responseConsumer. Any error encountered is handled by - * the errorConsumer. - * - * @param query query to be executed by the backend - * @param backendConnection connection to the backend - * @param componentsBuilder components builder containing the components relevant to the query execution - * @param protoBufIgnoredInputOutputs ProtoBuf object containing the inputs and outputs that should be ignored - * (can be null) - * @param responseConsumer consumer for handling the received response - * @param errorConsumer consumer for handling a potential error - */ - private void executeGrpcRequest(String query, - BackendConnection backendConnection, - QueryProtos.ComponentsUpdateRequest.Builder componentsBuilder, - QueryProtos.IgnoredInputOutputs protoBufIgnoredInputOutputs, - Consumer responseConsumer, - Consumer errorConsumer) { - StreamObserver observer = new StreamObserver<>() { - @Override - public void onNext(Empty value) { - } - - @Override - public void onError(Throwable t) { - errorConsumer.accept(t); - addBackendConnection(backendConnection); - } - - @Override - public void onCompleted() { - StreamObserver responseObserver = new StreamObserver<>() { - @Override - public void onNext(QueryProtos.QueryResponse value) { - responseConsumer.accept(value); - } - - @Override - public void onError(Throwable t) { - errorConsumer.accept(t); - addBackendConnection(backendConnection); - } - - @Override - public void onCompleted() { - addBackendConnection(backendConnection); - } - }; - - var queryBuilder = QueryProtos.Query.newBuilder() - .setId(0) - .setQuery(query); - - if (protoBufIgnoredInputOutputs != null) - queryBuilder.setIgnoredInputOutputs(protoBufIgnoredInputOutputs); - - backendConnection.getStub().withDeadlineAfter(deadlineForResponses, TimeUnit.MILLISECONDS) - .sendQuery(queryBuilder.build(), responseObserver); - } - }; - - backendConnection.getStub().withDeadlineAfter(deadlineForResponses, TimeUnit.MILLISECONDS) - .updateComponents(componentsBuilder.build(), observer); - } - - private void addBackendConnection(BackendConnection backendConnection) { - this.openBackendConnections.get(backendConnection.getBackendInstance()).add(backendConnection); + public void addBackendConnection(BackendConnection backendConnection) { + var relatedQueue = this.openBackendConnections.get(backendConnection.getBackendInstance()); + if (!relatedQueue.contains(backendConnection)) relatedQueue.add(backendConnection); } /** @@ -247,6 +74,17 @@ private BackendConnection getBackendConnection(BackendInstance backend) throws B return connection; } + /** + * Close all open backend connection and kill all locally running processes + * + * @throws IOException if any of the sockets do not respond + */ + public void closeAllBackendConnections() throws IOException { + for (BlockingQueue bq : openBackendConnections.values()) { + for (BackendConnection bc : bq) bc.close(); + } + } + /** * Attempts to start a new connection to the specified backend. On success, the backend is added to the associated * queue, otherwise, nothing happens. @@ -310,233 +148,53 @@ private void tryStartNewBackendConnection(BackendInstance backend) { addBackendConnection(newConnection); } - private void handleQueryResponse(QueryProtos.QueryResponse value, ExecutableQuery executableQuery) { - // If the query has been cancelled, ignore the result - if (executableQuery.queryListener.getQuery().getQueryState() == QueryState.UNKNOWN) return; - - if (value.hasRefinement() && value.getRefinement().getSuccess()) { - executableQuery.queryListener.getQuery().setQueryState(QueryState.SUCCESSFUL); - executableQuery.success.accept(true); - } else if (value.hasConsistency() && value.getConsistency().getSuccess()) { - executableQuery.queryListener.getQuery().setQueryState(QueryState.SUCCESSFUL); - executableQuery.success.accept(true); - } else if (value.hasDeterminism() && value.getDeterminism().getSuccess()) { - executableQuery.queryListener.getQuery().setQueryState(QueryState.SUCCESSFUL); - executableQuery.success.accept(true); - } else if (value.hasComponent()) { - executableQuery.queryListener.getQuery().setQueryState(QueryState.SUCCESSFUL); - executableQuery.success.accept(true); - JsonObject returnedComponent = (JsonObject) JsonParser.parseString(value.getComponent().getComponent().getJson()); - addGeneratedComponent(new Component(returnedComponent)); - } else { - executableQuery.queryListener.getQuery().setQueryState(QueryState.ERROR); - executableQuery.success.accept(false); - } - } - - private void handleQueryBackendError(Throwable t, ExecutableQuery executableQuery) { - // If the query has been cancelled, ignore the error - if (executableQuery.queryListener.getQuery().getQueryState() == QueryState.UNKNOWN) return; - - // Each error starts with a capitalized description of the error equal to the gRPC error type encountered - String errorType = t.getMessage().split(":\\s+", 2)[0]; - - switch (errorType) { - case "CANCELLED": - executableQuery.queryListener.getQuery().setQueryState(QueryState.ERROR); - executableQuery.failure.accept(new BackendException.QueryErrorException("The query was cancelled")); - break; - - case "DEADLINE_EXCEEDED": - executableQuery.queryListener.getQuery().setQueryState(QueryState.ERROR); - executableQuery.failure.accept(new BackendException.QueryErrorException("The backend did not answer the request in time")); - queryQueue.add(executableQuery); - break; - - case "UNIMPLEMENTED": - executableQuery.queryListener.getQuery().setQueryState(QueryState.SYNTAX_ERROR); - executableQuery.failure.accept(new BackendException.QueryErrorException("The query type is not supported by the backend")); - break; - - case "INTERNAL": - executableQuery.queryListener.getQuery().setQueryState(QueryState.ERROR); - executableQuery.failure.accept(new BackendException.QueryErrorException("The backend was unable to execute this query:\n" + t.getMessage().split(": ", 2)[1])); - break; - - case "UNKNOWN": - executableQuery.queryListener.getQuery().setQueryState(QueryState.ERROR); - executableQuery.failure.accept(new BackendException.QueryErrorException("The backend encountered an unknown error")); - break; - - case "UNAVAILABLE": - executableQuery.queryListener.getQuery().setQueryState(QueryState.SYNTAX_ERROR); - executableQuery.failure.accept(new BackendException.QueryErrorException("The backend could not be reached")); - break; - - default: - try { - executableQuery.queryListener.getQuery().setQueryState(QueryState.ERROR); - executableQuery.failure.accept(new BackendException.QueryErrorException("The query failed and gave the following error: " + errorType)); - } catch (Exception e) { - e.printStackTrace(); - } - break; - } - } - - private void addGeneratedComponent(Component newComponent) { - Platform.runLater(() -> { - newComponent.setTemporary(true); - - ObservableList listOfGeneratedComponents = Ecdar.getProject().getTempComponents(); - Component matchedComponent = null; - - for (Component currentGeneratedComponent : listOfGeneratedComponents) { - int comparisonOfNames = currentGeneratedComponent.getName().compareTo(newComponent.getName()); - - if (comparisonOfNames == 0) { - matchedComponent = currentGeneratedComponent; - break; - } else if (comparisonOfNames < 0) { - break; - } - } - - if (matchedComponent == null) { - UndoRedoStack.pushAndPerform(() -> { // Perform - Ecdar.getProject().getTempComponents().add(newComponent); - }, () -> { // Undo - Ecdar.getProject().getTempComponents().remove(newComponent); - }, "Created new component: " + newComponent.getName(), "add-circle"); - } else { - // Remove current component with name and add the newly generated one - Component finalMatchedComponent = matchedComponent; - UndoRedoStack.pushAndPerform(() -> { // Perform - Ecdar.getProject().getTempComponents().remove(finalMatchedComponent); - Ecdar.getProject().getTempComponents().add(newComponent); - }, () -> { // Undo - Ecdar.getProject().getTempComponents().remove(newComponent); - Ecdar.getProject().getTempComponents().add(finalMatchedComponent); - }, "Created new component: " + newComponent.getName(), "add-circle"); - } - - EcdarController.getActiveCanvasPresentation().getController().setActiveModel(newComponent); - }); - } - - private class ExecutableQuery { - private final String query; - private final BackendInstance backend; - private final Consumer success; - private final Consumer failure; - private final QueryListener queryListener; - public int tries = 0; - - ExecutableQuery(String query, BackendInstance backendInstance, Consumer success, Consumer failure, QueryListener queryListener) { - this.query = query; - this.backend = backendInstance; - this.success = success; - this.failure = failure; - this.queryListener = queryListener; - } - - /** - * Execute the query using the backend driver - */ - void execute(BackendConnection backendConnection) { - tries++; - executeQuery(this, backendConnection); - } - } - - private class BackendConnection { - private final Process process; - private final EcdarBackendGrpc.EcdarBackendStub stub; - private final ManagedChannel channel; - private final BackendInstance backendInstance; - - BackendConnection(BackendInstance backendInstance, Process process, EcdarBackendGrpc.EcdarBackendStub stub, ManagedChannel channel) { - this.process = process; - this.stub = stub; - this.backendInstance = backendInstance; - this.channel = channel; - } - - /** - * Get the gRPC stub of the connection to use for query execution - * - * @return the gRPC stub of this connection - */ - public EcdarBackendGrpc.EcdarBackendStub getStub() { - return stub; - } - - /** - * Get the backend instance that should be used to execute - * the query currently associated with this backend connection - * - * @return the instance of the associated executable query object, - * or null, if no executable query is currently associated - */ - public BackendInstance getBackendInstance() { - return backendInstance; - } - - /** - * Close the gRPC connection and end the process - * - * @throws IOException originally thrown by the destroy method on java.lang.Process - */ - public void close() throws IOException { - if (!channel.isShutdown()) { - try { - channel.shutdown(); - if (!channel.awaitTermination(45, TimeUnit.SECONDS)) { - channel.shutdownNow(); // Forcefully close the connection - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - // If the backend-instance is remote, there will not be a process - if (process != null) { - process.destroy(); - } - - openBackendConnections.get(backendInstance).remove(this); - } - } - - private class QueryConsumer implements Runnable { - BlockingQueue queue; - - private QueryConsumer(BlockingQueue queue) { - this.queue = queue; - } - +// public SimulationState getInitialSimulationState() { +// SimulationState state = new SimulationState(ObjectProtos.State.newBuilder().getDefaultInstanceForType()); +// state.getLocations().add(new Pair<>(Ecdar.getProject().getComponents().get(0).getName(), Ecdar.getProject().getComponents().get(0).getLocations().get(0).getId())); +// return state; +// } + + // private class ExecutableStartSimRequest { + // private final String componentComposition; + // private final BackendInstance backendInstance; + // private final Consumer success; + // private final Consumer failure; + // private final StartSimListener startSimListener; + // public int tries = 0; + + // public ExecutableStartSimRequest(String componentComposition, BackendInstance backendInstance, + // Consumer success, Consumer failure, StartSimListener startSimListener, + // int tries) { + // this.componentComposition = componentComposition; + // this.backendInstance = backendInstance; + // this.success = success; + // this.failure = failure; + // this.startSimListener = startSimListener; + // this.tries = tries; + // } + // } + + private class GrpcRequestConsumer implements Runnable { @Override public void run() { while (true) { try { - ExecutableQuery executableQuery = this.queue.take(); + GrpcRequest request = requestQueue.take(); - final BackendConnection backendConnection; try { - backendConnection = getBackendConnection(executableQuery.backend); - executableQuery.execute(backendConnection); + request.tries++; + request.execute(getBackendConnection(request.getBackend())); } catch (BackendException.NoAvailableBackendConnectionException e) { e.printStackTrace(); - if (executableQuery.tries < numberOfRetriesPerQuery) { + if (request.tries < numberOfRetriesPerQuery) { new Timer().schedule(new TimerTask() { @Override public void run() { - queryQueue.add(executableQuery); + requestQueue.add(request); } - }, rerunQueryDelay); + }, rerunRequestDelay); } else { - executableQuery.failure.accept(new BackendException("Failed to execute query after five tries")); - executableQuery.queryListener.getQuery().setQueryState(QueryState.ERROR); + Ecdar.showToast("Unable to find a connection to the requested engine"); } return; } @@ -546,13 +204,4 @@ public void run() { } } } - - enum TraceType { - NONE, SOME, SHORTEST, FASTEST; - - @Override - public String toString() { - return "trace " + this.ordinal(); - } - } } diff --git a/src/main/java/ecdar/backend/BackendHelper.java b/src/main/java/ecdar/backend/BackendHelper.java index f9ee6a35..0397b6ee 100644 --- a/src/main/java/ecdar/backend/BackendHelper.java +++ b/src/main/java/ecdar/backend/BackendHelper.java @@ -1,5 +1,6 @@ package ecdar.backend; +import EcdarProtoBuf.ComponentProtos; import ecdar.Ecdar; import ecdar.abstractions.*; import javafx.beans.property.SimpleListProperty; @@ -18,6 +19,8 @@ import java.util.List; import java.util.Optional; +import static ecdar.controllers.SimulationInitializationDialogController.ListOfComponents; + public final class BackendHelper { final static String TEMP_DIRECTORY = "temporary"; private static BackendInstance defaultBackend = null; @@ -46,16 +49,6 @@ public static String storeQuery(String query, String fileName) throws URISyntaxE return path; } - /** - * Check if the given backend supports ignored inputs and outputs as parameters. - * - * @param backend the name of the backend to check - * @return true if the backend supports ignored inputs and outputs, else false - */ - public static Boolean backendSupportsInputOutputs(BackendInstance backend) { - return true; - } - /** * Gets the directory path for storing temporary files. * @@ -76,12 +69,60 @@ public static void stopQueries() { /** * Generates a reachability query based on the given location and component * - * @param location The location which should be checked for reachability - * @param component The component where the location belong to / are placed + * @param endLocation The location which should be checked for reachability * @return A reachability query string */ - public static String getLocationReachableQuery(final Location location, final Component component) { - return component.getName() + "." + location.getId(); + public static String getLocationReachableQuery(final Location endLocation, final Component component, final String query) { + var stringBuilder = new StringBuilder(); + + // append simulation query + stringBuilder.append(query); + + // append start location here TODO + + // append end state + stringBuilder.append(getEndStateString(component.getName(), endLocation.getId())); + + // append clocks + stringBuilder.append("("); + // append clock here TODO + stringBuilder.append(")"); + + // return example: m1||M2->[L1,L4](y<3);[L2, L7](y<2) + return stringBuilder.toString(); + } + + private static String getEndStateString(String componentName, String endLocationId) { + var stringBuilder = new StringBuilder(); + + stringBuilder.append(" -> ["); + var appendLocationWithSeparator = false; + + for (var component:ListOfComponents) + { + if (component.equals(componentName)){ + if (appendLocationWithSeparator){ + stringBuilder.append("," + endLocationId); + } + else{ + stringBuilder.append(endLocationId); + } + } + else{ // add underscore to indicate, that we don't care about the end locations in the other components + if (appendLocationWithSeparator){ + stringBuilder.append(",_"); + } + else{ + stringBuilder.append("_"); + } + } + if (!appendLocationWithSeparator) { + appendLocationWithSeparator = true; + } + } + stringBuilder.append("]"); + + return stringBuilder.toString(); } /** @@ -156,4 +197,15 @@ public static void setDefaultBackendInstance(BackendInstance newDefaultBackend) public static void addBackendInstanceListener(Runnable runnable) { BackendHelper.backendInstancesUpdatedListeners.add(runnable); } + + public static ComponentProtos.ComponentsInfo.Builder getComponentsInfoBuilder(String query) { + ComponentProtos.ComponentsInfo.Builder componentsInfoBuilder = ComponentProtos.ComponentsInfo.newBuilder(); + for (Component c : Ecdar.getProject().getComponents()) { + if (query.contains(c.getName())) { + componentsInfoBuilder.addComponents(ComponentProtos.Component.newBuilder().setJson(c.serialize().toString()).build()); + } + } + componentsInfoBuilder.setComponentsHash(componentsInfoBuilder.getComponentsList().hashCode()); + return componentsInfoBuilder; + } } diff --git a/src/main/java/ecdar/backend/GrpcRequest.java b/src/main/java/ecdar/backend/GrpcRequest.java new file mode 100644 index 00000000..100bd810 --- /dev/null +++ b/src/main/java/ecdar/backend/GrpcRequest.java @@ -0,0 +1,24 @@ +package ecdar.backend; + +import ecdar.abstractions.BackendInstance; + +import java.util.function.Consumer; + +public class GrpcRequest { + private final Consumer request; + private final BackendInstance backend; + public int tries = 0; + + public GrpcRequest(Consumer request, BackendInstance backend) { + this.request = request; + this.backend = backend; + } + + public void execute(BackendConnection backendConnection) { + this.request.accept(backendConnection); + } + + public BackendInstance getBackend() { + return backend; + } +} \ No newline at end of file diff --git a/src/main/java/ecdar/backend/IgnoredInputOutputQuery.java b/src/main/java/ecdar/backend/IgnoredInputOutputQuery.java deleted file mode 100644 index 06843b31..00000000 --- a/src/main/java/ecdar/backend/IgnoredInputOutputQuery.java +++ /dev/null @@ -1,49 +0,0 @@ -package ecdar.backend; - -import ecdar.abstractions.Query; -import ecdar.presentations.QueryPresentation; -import javafx.scene.layout.VBox; - -import java.util.ArrayList; -import java.util.HashMap; - -public class IgnoredInputOutputQuery { - private final Query query; - private final QueryPresentation queryPresentation; - private final HashMap ignoredInputs; - private final VBox ignoredInputsVBox; - private final HashMap ignoredOutputs; - private final VBox ignoredOutputsVBox; - public int tries = 0; - - public IgnoredInputOutputQuery(Query query, QueryPresentation queryPresentation, HashMap ignoredInputs, VBox ignoredInputsVBox, HashMap ignoredOutputs, VBox ignoredOutputsVBox) { - this.query = query; - this.queryPresentation = queryPresentation; - this.ignoredInputs = ignoredInputs; - this.ignoredInputsVBox = ignoredInputsVBox; - this.ignoredOutputs = ignoredOutputs; - this.ignoredOutputsVBox = ignoredOutputsVBox; - } - - public Query getQuery() { - return query; - } - - public void addNewElementsToMap(ArrayList inputs, ArrayList outputs) { - // Add inputs to list and as checkboxes in UI - for (String key : inputs) { - if (!this.ignoredInputs.containsKey(key)) { - this.queryPresentation.addInputOrOutput(key, false, this.ignoredInputs, this.ignoredInputsVBox); - this.ignoredInputs.put(key, false); - } - } - - // Add inputs to list and as checkboxes in UI - for (String key : outputs) { - if (!this.ignoredInputs.containsKey(key)) { - this.queryPresentation.addInputOrOutput(key, false, this.ignoredInputs, this.ignoredInputsVBox); - this.ignoredInputs.put(key, false); - } - } - } -} diff --git a/src/main/java/ecdar/backend/QueryHandler.java b/src/main/java/ecdar/backend/QueryHandler.java new file mode 100644 index 00000000..a2f6162e --- /dev/null +++ b/src/main/java/ecdar/backend/QueryHandler.java @@ -0,0 +1,281 @@ +package ecdar.backend; + +import EcdarProtoBuf.QueryProtos; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import ecdar.Ecdar; +import ecdar.abstractions.Component; +import ecdar.abstractions.Query; +import ecdar.abstractions.QueryState; +import ecdar.abstractions.QueryType; +import ecdar.controllers.EcdarController; +import ecdar.utility.UndoRedoStack; +import ecdar.utility.helpers.StringValidator; +import io.grpc.stub.StreamObserver; +import javafx.application.Platform; +import javafx.collections.ObservableList; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.NoSuchElementException; +import java.util.concurrent.TimeUnit; + +public class QueryHandler { + private final BackendDriver backendDriver; + private final ArrayList connections = new ArrayList<>(); + + public QueryHandler(BackendDriver backendDriver) { + this.backendDriver = backendDriver; + } + + /** + * Executes the specified query + * @param query query to be executed + */ + public void executeQuery(Query query) throws NoSuchElementException { + if (query.getQueryState().equals(QueryState.RUNNING) || !StringValidator.validateQuery(query.getQuery())) return; + + if (query.getQuery().isEmpty()) { + query.setQueryState(QueryState.SYNTAX_ERROR); + query.addError("Query is empty"); + return; + } + + query.setQueryState(QueryState.RUNNING); + query.errors().set(""); + + GrpcRequest request = new GrpcRequest(backendConnection -> { + connections.add(backendConnection); // Save reference for closing connection on exit + + var componentsInfoBuilder = BackendHelper.getComponentsInfoBuilder(query.getQuery()); + + StreamObserver responseObserver = new StreamObserver<>() { + @Override + public void onNext(QueryProtos.QueryResponse value) { + handleQueryResponse(value, query); + } + + @Override + public void onError(Throwable t) { + handleQueryBackendError(t, query); + + // Release backend connection + backendDriver.addBackendConnection(backendConnection); + connections.remove(backendConnection); + } + + @Override + public void onCompleted() { + // Release backend connection + backendDriver.addBackendConnection(backendConnection); + connections.remove(backendConnection); + } + }; + + // ToDo SW5: Not working with the updated gRPC Protos + var queryBuilder = QueryProtos.QueryRequest.newBuilder() + .setUserId(1) + .setQueryId(1) + .setQuery(query.getType().getQueryName() + ": " + query.getQuery()) + .setComponentsInfo(componentsInfoBuilder); + + backendConnection.getStub().withDeadlineAfter(backendDriver.getResponseDeadline(), TimeUnit.MILLISECONDS) + .sendQuery(queryBuilder.build(), responseObserver); + }, query.getBackend()); + + backendDriver.addRequestToExecutionQueue(request); + } + + /** + * Close all open backend connection and kill all locally running processes + * + * @throws IOException if any of the sockets do not respond + */ + public void closeAllBackendConnections() throws IOException { + for (BackendConnection con : connections) { + con.close(); + } + } + + private void handleQueryResponse(QueryProtos.QueryResponse value, Query query) { + System.out.println(value); + // If the query has been cancelled, ignore the result + if (query.getQueryState() == QueryState.UNKNOWN) return; + switch (value.getResponseCase()) { + case QUERY_OK: + QueryProtos.QueryResponse.QueryOk queryOk = value.getQueryOk(); + switch (queryOk.getResultCase()) { + case REFINEMENT: + if (queryOk.getRefinement().getSuccess()) { + query.setQueryState(QueryState.SUCCESSFUL); + query.getSuccessConsumer().accept(true); + } else { + query.setQueryState(QueryState.ERROR); + query.getFailureConsumer().accept(new BackendException.QueryErrorException(queryOk.getRefinement().getReason())); + query.getSuccessConsumer().accept(false); + query.getStateActionConsumer().accept(value.getQueryOk().getRefinement().getState(), + value.getQueryOk().getRefinement().getAction()); + } + break; + + case CONSISTENCY: + if (queryOk.getConsistency().getSuccess()) { + query.setQueryState(QueryState.SUCCESSFUL); + query.getSuccessConsumer().accept(true); + } else { + query.setQueryState(QueryState.ERROR); + query.getFailureConsumer().accept(new BackendException.QueryErrorException(queryOk.getConsistency().getReason())); + query.getSuccessConsumer().accept(false); + query.getStateActionConsumer().accept(value.getQueryOk().getConsistency().getState(), + value.getQueryOk().getConsistency().getAction()); + + } + break; + + case DETERMINISM: + if (queryOk.getDeterminism().getSuccess()) { + query.setQueryState(QueryState.SUCCESSFUL); + query.getSuccessConsumer().accept(true); + } else { + query.setQueryState(QueryState.ERROR); + query.getFailureConsumer().accept(new BackendException.QueryErrorException(queryOk.getDeterminism().getReason())); + query.getSuccessConsumer().accept(false); + query.getStateActionConsumer().accept(value.getQueryOk().getDeterminism().getState(), + value.getQueryOk().getDeterminism().getAction()); + + } + break; + + case IMPLEMENTATION: + if (queryOk.getImplementation().getSuccess()) { + query.setQueryState(QueryState.SUCCESSFUL); + query.getSuccessConsumer().accept(true); + } else { + query.setQueryState(QueryState.ERROR); + query.getFailureConsumer().accept(new BackendException.QueryErrorException(queryOk.getImplementation().getReason())); + query.getSuccessConsumer().accept(false); + //ToDo: These errors are not implemented in the Reveaal backend. + query.getStateActionConsumer().accept(value.getQueryOk().getImplementation().getState(), + ""); + } + break; + + case REACHABILITY: + if (queryOk.getReachability().getSuccess()) { + query.setQueryState(QueryState.SUCCESSFUL); + if(value.getQueryOk().getReachability().getSuccess()){ + Ecdar.showToast("Reachability check was successful and the location can be reached."); + } + else if(!value.getQueryOk().getReachability().getSuccess()){ + Ecdar.showToast("Reachability check was successful but the location cannot be reached."); + } + query.getSuccessConsumer().accept(true); + } else { + query.setQueryState(QueryState.ERROR); + Ecdar.showToast("Reachability check was unsuccessful!"); + query.getFailureConsumer().accept(new BackendException.QueryErrorException(queryOk.getReachability().getReason())); + query.getSuccessConsumer().accept(false); + //ToDo: These errors are not implemented in the Reveaal backend. + query.getStateActionConsumer().accept(value.getQueryOk().getReachability().getState(), + ""); + } + break; + + case COMPONENT: + query.setQueryState(QueryState.SUCCESSFUL); + query.getSuccessConsumer().accept(true); + JsonObject returnedComponent = (JsonObject) JsonParser.parseString(queryOk.getComponent().getComponent().getJson()); + addGeneratedComponent(new Component(returnedComponent)); + break; + + case ERROR: + query.setQueryState(QueryState.ERROR); + query.getFailureConsumer().accept(new BackendException.QueryErrorException(queryOk.getError())); + query.getSuccessConsumer().accept(false); + break; + + case RESULT_NOT_SET: + query.setQueryState(QueryState.ERROR); + query.getSuccessConsumer().accept(false); + break; + } + break; + + case USER_TOKEN_ERROR: + query.setQueryState(QueryState.ERROR); + query.getFailureConsumer().accept(new BackendException.QueryErrorException(value.getUserTokenError().getErrorMessage())); + query.getSuccessConsumer().accept(false); + break; + + case RESPONSE_NOT_SET: + query.setQueryState(QueryState.ERROR); + query.getSuccessConsumer().accept(false); + break; + } + } + + private void handleQueryBackendError(Throwable t, Query query) { + // If the query has been cancelled, ignore the error + if (query.getQueryState() == QueryState.UNKNOWN) return; + + // due to lack of information from backend if the reachability check shows that a location can NOT be reached, this is the most accurate information we can provide + if(query.getType() == QueryType.REACHABILITY){ + Ecdar.showToast("The reachability query failed. This might be due to the fact that the location is not reachable."); + } + + // Each error starts with a capitalized description of the error equal to the gRPC error type encountered + String errorType = t.getMessage().split(":\\s+", 2)[0]; + + if ("DEADLINE_EXCEEDED".equals(errorType)) { + query.setQueryState(QueryState.ERROR); + query.getFailureConsumer().accept(new BackendException.QueryErrorException("The backend did not answer the request in time")); + } else { + try { + query.setQueryState(QueryState.ERROR); + query.getFailureConsumer().accept(new BackendException.QueryErrorException("The execution of this query failed with message:\n" + t.getLocalizedMessage())); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private void addGeneratedComponent(Component newComponent) { + Platform.runLater(() -> { + newComponent.setTemporary(true); + + ObservableList listOfGeneratedComponents = Ecdar.getProject().getTempComponents(); // ToDo NIELS: Refactor + Component matchedComponent = null; + + for (Component currentGeneratedComponent : listOfGeneratedComponents) { + int comparisonOfNames = currentGeneratedComponent.getName().compareTo(newComponent.getName()); + + if (comparisonOfNames == 0) { + matchedComponent = currentGeneratedComponent; + break; + } else if (comparisonOfNames < 0) { + break; + } + } + + if (matchedComponent == null) { + UndoRedoStack.pushAndPerform(() -> { // Perform + Ecdar.getProject().getTempComponents().add(newComponent); + }, () -> { // Undo + Ecdar.getProject().getTempComponents().remove(newComponent); + }, "Created new component: " + newComponent.getName(), "add-circle"); + } else { + // Remove current component with name and add the newly generated one + Component finalMatchedComponent = matchedComponent; + UndoRedoStack.pushAndPerform(() -> { // Perform + Ecdar.getProject().getTempComponents().remove(finalMatchedComponent); + Ecdar.getProject().getTempComponents().add(newComponent); + }, () -> { // Undo + Ecdar.getProject().getTempComponents().remove(newComponent); + Ecdar.getProject().getTempComponents().add(finalMatchedComponent); + }, "Created new component: " + newComponent.getName(), "add-circle"); + } + + EcdarController.getActiveCanvasPresentation().getController().setActiveModel(newComponent); + }); + } +} diff --git a/src/main/java/ecdar/backend/QueryListener.java b/src/main/java/ecdar/backend/QueryListener.java deleted file mode 100644 index ffdccc3a..00000000 --- a/src/main/java/ecdar/backend/QueryListener.java +++ /dev/null @@ -1,45 +0,0 @@ -package ecdar.backend; - -import ecdar.abstractions.Query; -import ecdar.abstractions.QueryState; -import ecdar.controllers.EcdarController; -import javafx.application.Platform; - -public class QueryListener { - - private final Query query; - - public QueryListener() { - this(new Query("Unknown", "Unknown", QueryState.UNKNOWN)); - } - - public QueryListener(final Query query) { - this.query = query; - } - - public void setLength(final int i) { - - } - - public void setCurrent(final int i) { - - } - - public Query getQuery() { - return query; - } - - /* - public void setTrace(char c, String s, Vector vector, int i) { - - } - */ - - public void setFeedback(final String s) { - if (s.contains("inf") || s.contains("sup")) { - Platform.runLater(() -> { - EcdarController.openQueryDialog(query, s.split("\n")[1]); - }); - } - } -} diff --git a/src/main/java/ecdar/backend/SimulationHandler.java b/src/main/java/ecdar/backend/SimulationHandler.java new file mode 100644 index 00000000..b15e96b0 --- /dev/null +++ b/src/main/java/ecdar/backend/SimulationHandler.java @@ -0,0 +1,349 @@ +package ecdar.backend; + +import EcdarProtoBuf.ComponentProtos; +import EcdarProtoBuf.ObjectProtos; +import EcdarProtoBuf.QueryProtos; +import EcdarProtoBuf.ObjectProtos.Decision; +import ecdar.Ecdar; +import ecdar.abstractions.*; +import ecdar.simulation.SimulationState; +import io.grpc.stub.StreamObserver; +import javafx.application.Platform; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import javafx.util.Pair; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import EcdarProtoBuf.QueryProtos.SimulationInfo; +import EcdarProtoBuf.QueryProtos.SimulationStepRequest; +import EcdarProtoBuf.QueryProtos.SimulationStepResponse; + +/** + * Handles state changes, updates of values / clocks, and keeps track of all the transitions that + * have been taken throughout a simulation. + */ +public class SimulationHandler { + public static final String QUERY_PREFIX = "Query: "; + private String composition; + public ObjectProperty currentState = new SimpleObjectProperty<>(); + public ObjectProperty initialState = new SimpleObjectProperty<>(); + public ObjectProperty selectedEdge = new SimpleObjectProperty<>(); + private EcdarSystem system; + private int numberOfSteps; + + private final ObservableMap simulationVariables = FXCollections.observableHashMap(); + private final ObservableMap simulationClocks = FXCollections.observableHashMap(); + public ObservableList traceLog = FXCollections.observableArrayList(); + private final BackendDriver backendDriver; + private final ArrayList connections = new ArrayList<>(); + + /** + * Empty constructor that should be used if the system or project has not be initialized yet + */ + public SimulationHandler(BackendDriver backendDriver) { + this.backendDriver = backendDriver; + } + + + /** + * Initializes the values and properties in the {@link SimulationHandler}. + * Can also be used as a reset of the simulation. + * THIS METHOD DOES NOT RESET THE ENGINE, + */ + private void initializeSimulation() { + // Initialization + this.numberOfSteps = 0; + this.simulationVariables.clear(); + this.simulationClocks.clear(); + this.currentState.set(null); + this.selectedEdge.set(null); + this.traceLog.clear(); + + this.system = getSystem(); + } + + + /** + * Reloads the whole simulation sets the initial transitions, states, etc + */ + public void initialStep() { + initializeSimulation(); + + GrpcRequest request = new GrpcRequest(backendConnection -> { + StreamObserver responseObserver = new StreamObserver<>() { + @Override + public void onNext(QueryProtos.SimulationStepResponse value) { + currentState.set(new SimulationState(value.getNewDecisionPoint())); + Platform.runLater(() -> traceLog.add(currentState.get())); + } + + @Override + public void onError(Throwable t) { + Ecdar.showToast("Could not start simulation:\n" + t.getMessage()); + + // Release backend connection + backendDriver.addBackendConnection(backendConnection); + connections.remove(backendConnection); + } + + @Override + public void onCompleted() { + // Release backend connection + backendDriver.addBackendConnection(backendConnection); + connections.remove(backendConnection); + } + }; + + var comInfo = ComponentProtos.ComponentsInfo.newBuilder(); + for (Component c : Ecdar.getProject().getComponents()) { + comInfo.addComponents(ComponentProtos.Component.newBuilder().setJson(c.serialize().toString()).build()); + } + comInfo.setComponentsHash(comInfo.getComponentsList().hashCode()); + var simStartRequest = QueryProtos.SimulationStartRequest.newBuilder(); + var simInfo = QueryProtos.SimulationInfo.newBuilder() + .setComponentComposition(composition) + .setComponentsInfo(comInfo); + simStartRequest.setSimulationInfo(simInfo); + backendConnection.getStub().withDeadlineAfter(this.backendDriver.getResponseDeadline(), TimeUnit.MILLISECONDS) + .startSimulation(simStartRequest.build(), responseObserver); + }, BackendHelper.getDefaultBackendInstance()); + + backendDriver.addRequestToExecutionQueue(request); + + //Save the previous states, and get the new + this.traceLog.add(currentState.get()); + numberOfSteps++; + + //Updates the transitions available + updateAllValues(); + + } + + /** + * Resets the simulation to the initial location + */ + public void resetToInitialLocation() { + initialStep(); + } + + /** + * Take a step in the simulation. + */ + public void nextStep() { + GrpcRequest request = new GrpcRequest(backendConnection -> { + StreamObserver responseObserver = new StreamObserver<>() { + @Override + public void onNext(QueryProtos.SimulationStepResponse value) { + currentState.set(new SimulationState(value.getNewDecisionPoint())); + Platform.runLater(() -> traceLog.add(currentState.get())); + } + + @Override + public void onError(Throwable t) { + Ecdar.showToast("Could not take next step in simulation\nError: " + t.getMessage()); + + // Release backend connection + backendDriver.addBackendConnection(backendConnection); + connections.remove(backendConnection); + } + + @Override + public void onCompleted() { + // Release backend connection + backendDriver.addBackendConnection(backendConnection); + connections.remove(backendConnection); + } + }; + + var comInfo = ComponentProtos.ComponentsInfo.newBuilder(); + for (Component c : Ecdar.getProject().getComponents()) { + comInfo.addComponents(ComponentProtos.Component.newBuilder().setJson(c.serialize().toString()).build()); + } + comInfo.setComponentsHash(comInfo.getComponentsList().hashCode()); + var simStepRequest = SimulationStepRequest.newBuilder(); + var simInfo = SimulationInfo.newBuilder() + .setComponentComposition(composition) + .setComponentsInfo(comInfo); + simStepRequest.setSimulationInfo(simInfo); + var source = currentState.get().getState(); + var specComp = ObjectProtos.SpecificComponent.newBuilder().setComponentName(getComponentName(selectedEdge.get())).setComponentIndex(getComponentIndex(selectedEdge.get())); + var edge = EcdarProtoBuf.ObjectProtos.Edge.newBuilder().setId(selectedEdge.get().getId()).setSpecificComponent(specComp); + var decision = Decision.newBuilder().setEdge(edge).setSource(source); + simStepRequest.setChosenDecision(decision); + + backendConnection.getStub().withDeadlineAfter(this.backendDriver.getResponseDeadline(), TimeUnit.MILLISECONDS) + .takeSimulationStep(simStepRequest.build(), responseObserver); + }, BackendHelper.getDefaultBackendInstance()); + + backendDriver.addRequestToExecutionQueue(request); + + + // increments the number of steps taken during this simulation + numberOfSteps++; + + + updateAllValues(); + } + + private String getComponentName(Edge edge) { + var components = Ecdar.getProject().getComponents(); + for (var component : components) { + for (var e : component.getEdges()) { + if (e.getId().equals(edge.getId())) { + return component.getName(); + } + } + } + throw new RuntimeException("Could not find component name for edge with id " + edge.getId()); + } + + private int getComponentIndex (Edge edge) { + for (int i = 0; i < Ecdar.getProject().getComponents().size(); i++) { + if (Ecdar.getProject().getComponents().get(i).getEdges().stream().anyMatch(p -> p.getId() == edge.getId())) { + return i; + } + }; + throw new IllegalArgumentException("Edge does not belong to any component"); + } + + + /** + * Updates all values and clocks that are used doing the current simulation. + * It also stores the variables in the {@link SimulationHandler#simulationVariables} + * and the clocks in {@link SimulationHandler#simulationClocks}. + */ + private void updateAllValues() { + setSimVarAndClocks(); + } + + /** + * Sets the value of simulation variables and clocks, based on {@link SimulationHandler#currentConcreteState} + */ + private void setSimVarAndClocks() { + // The variables and clocks are all found in the getVariables array + // the array is always of the following order: variables, clocks. + // The noOfVars variable thus also functions as an offset for the clocks in the getVariables array +// final int noOfClocks = engine.getSystem().getNoOfClocks(); +// final int noOfVars = engine.getSystem().getNoOfVariables(); + +// for (int i = 0; i < noOfVars; i++){ +// simulationVariables.put(engine.getSystem().getVariableName(i), +// currentConcreteState.get().getVariables()[i].getValue(BigDecimal.ZERO)); +// } + + // As the clocks values starts after the variables values in currentConcreteState.get().getVariables() + // Then i needs to start where the variables ends. + // j is needed to map the correct name with the value +// for (int i = noOfVars, j = 0; i < noOfClocks + noOfVars ; i++, j++) { +// simulationClocks.put(engine.getSystem().getClockName(j), +// currentConcreteState.get().getVariables()[i].getValue(BigDecimal.ZERO)); +// } + } + + + + /** + * The number of total steps taken in the current simulation + * + * @return the number of steps + */ + public int getNumberOfSteps() { + return numberOfSteps; + } + + /** + * All the transitions taken in this simulation + * + * @return an {@link ObservableList} of all the transitions taken in this simulation so far + */ + public ObservableList getTraceLog() { + return traceLog; + } + + /** + * All the available transitions in this state + * @return + * + * @return an {@link ObservableList} of all the currently available transitions in this state + */ + public ArrayList> getAvailableTransitions() { + return currentState.get().getEdges(); + } + + /** + * All the variables connected to the current simulation. + * This does not return any clocks, if you need please use {@link SimulationHandler#getSimulationClocks()} instead + * + * @return a {@link Map} where the name (String) is the key, and a {@link BigDecimal} is the value + */ + public ObservableMap getSimulationVariables() { + return simulationVariables; + } + + /** + * All the clocks connected to the current simulation. + * + * @return a {@link Map} where the name (String) is the key, and a {@link BigDecimal} is the clock value + * @see SimulationHandler#getSimulationVariables() + */ + public ObservableMap getSimulationClocks() { + return simulationClocks; + } + + /** + * The initial state of the current simulation + * + * @return the initial {@link SimulationState} of this simulation + */ + public SimulationState getInitialState() { + // ToDo: Implement + return initialState.get(); + } + + public ObjectProperty initialStateProperty() { + return initialState; + } + + + public EcdarSystem getSystem() { + return system; + } + + public String getComposition() { return composition;} + + public void setComposition(String composition) {this.composition = composition;} + + public boolean isSimulationRunning() { + return false; // ToDo: Implement + } + + /** + * Close all open backend connection and kill all locally running processes + * + * @throws IOException if any of the sockets do not respond + */ + public void closeAllBackendConnections() throws IOException { + for (BackendConnection con : connections) { + con.close(); + } + } + + + /** + * Sets the current state of the simulation to the given state from the trace log + */ + public void selectStateFromLog(SimulationState state) { + while (traceLog.get(traceLog.size() - 1) != state) { + traceLog.remove(traceLog.size() - 1); + } + currentState.set(state); + } +} \ No newline at end of file diff --git a/src/main/java/ecdar/controllers/BackendOptionsDialogController.java b/src/main/java/ecdar/controllers/BackendOptionsDialogController.java index 8d880858..4810e8cd 100644 --- a/src/main/java/ecdar/controllers/BackendOptionsDialogController.java +++ b/src/main/java/ecdar/controllers/BackendOptionsDialogController.java @@ -73,6 +73,7 @@ public boolean saveChangesToBackendOptions() { // Close all backend connections to avoid dangling backend connections when port range is changed try { Ecdar.getBackendDriver().closeAllBackendConnections(); + Ecdar.getQueryExecutor().closeAllBackendConnections(); } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/ecdar/controllers/ComponentController.java b/src/main/java/ecdar/controllers/ComponentController.java index 40d239b6..24a546d4 100644 --- a/src/main/java/ecdar/controllers/ComponentController.java +++ b/src/main/java/ecdar/controllers/ComponentController.java @@ -13,7 +13,6 @@ import javafx.animation.Interpolator; import javafx.animation.Transition; import javafx.application.Platform; -import javafx.beans.binding.DoubleBinding; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; @@ -329,7 +328,7 @@ private void initializeContextMenu() { }, "Added universal location '" + newLocation + "' to component '" + component.getName() + "'", "add-circle"); }); - // Adds the add inconsistent location element to the drop down menu, this element adds an inconsistent location + // Adds the add inconsistent location element to the dropdown menu, this element adds an inconsistent location contextMenu.addClickableListElement("Add Inconsistent Location", event -> { contextMenu.hide(); double x = DropDownMenu.x - LocationPresentation.RADIUS / 2; @@ -359,7 +358,7 @@ private void initializeContextMenu() { final Query query = new Query(deadlockQuery, deadlockComment, QueryState.UNKNOWN); query.setType(QueryType.REACHABILITY); Ecdar.getProject().getQueries().add(query); - query.run(); + Ecdar.getQueryExecutor().executeQuery(query); contextMenu.hide(); }); diff --git a/src/main/java/ecdar/controllers/ComponentOperatorController.java b/src/main/java/ecdar/controllers/ComponentOperatorController.java index f0b7c606..7da44f1c 100644 --- a/src/main/java/ecdar/controllers/ComponentOperatorController.java +++ b/src/main/java/ecdar/controllers/ComponentOperatorController.java @@ -14,7 +14,6 @@ import javafx.scene.control.Label; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; -import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.shape.Circle; import javafx.scene.shape.Polygon; diff --git a/src/main/java/ecdar/controllers/DeclarationsController.java b/src/main/java/ecdar/controllers/DeclarationsController.java index 4b55672a..b4ab05e0 100644 --- a/src/main/java/ecdar/controllers/DeclarationsController.java +++ b/src/main/java/ecdar/controllers/DeclarationsController.java @@ -43,11 +43,10 @@ public void initialize(final URL location, final ResourceBundle resources) { */ private void initializeWidthAndHeight() { // Fetch width and height of canvas and update - root.minWidthProperty().bind(Ecdar.getPresentation().getController().canvasPane.minWidthProperty()); - root.maxWidthProperty().bind(Ecdar.getPresentation().getController().canvasPane.maxWidthProperty()); - root.minHeightProperty().bind(Ecdar.getPresentation().getController().canvasPane.minHeightProperty()); - root.maxHeightProperty().bind(Ecdar.getPresentation().getController().canvasPane.maxHeightProperty()); - textArea.setTranslateY(20); + root.minWidthProperty().bind(Ecdar.getPresentation().getController().getEditorPresentation().getController().canvasPane.minWidthProperty()); + root.maxWidthProperty().bind(Ecdar.getPresentation().getController().getEditorPresentation().getController().canvasPane.maxWidthProperty()); + root.minHeightProperty().bind(Ecdar.getPresentation().getController().getEditorPresentation().getController().canvasPane.minHeightProperty()); + root.maxHeightProperty().bind(Ecdar.getPresentation().getController().getEditorPresentation().getController().canvasPane.maxHeightProperty()); } /** diff --git a/src/main/java/ecdar/controllers/EcdarController.java b/src/main/java/ecdar/controllers/EcdarController.java index e350b296..89ebb802 100644 --- a/src/main/java/ecdar/controllers/EcdarController.java +++ b/src/main/java/ecdar/controllers/EcdarController.java @@ -6,13 +6,11 @@ import ecdar.Ecdar; import ecdar.abstractions.*; import ecdar.backend.BackendHelper; -import ecdar.backend.QueryListener; import ecdar.code_analysis.CodeAnalysis; import ecdar.mutation.models.MutationTestPlan; import ecdar.presentations.*; import ecdar.utility.UndoRedoStack; import ecdar.utility.colors.Color; -import ecdar.utility.colors.EnabledColor; import ecdar.utility.helpers.SelectHelper; import ecdar.utility.keyboard.Keybind; import ecdar.utility.keyboard.KeyboardTracker; @@ -23,7 +21,6 @@ import javafx.beans.binding.When; import javafx.beans.property.*; import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; import javafx.embed.swing.SwingFXUtils; import javafx.fxml.FXML; import javafx.fxml.Initializable; @@ -39,7 +36,6 @@ import javafx.scene.text.Text; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; -import javafx.util.Pair; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; @@ -49,6 +45,7 @@ import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.stream.Collectors; public class EcdarController implements Initializable { // Reachability analysis @@ -56,29 +53,18 @@ public class EcdarController implements Initializable { private static long reachabilityTime = Long.MAX_VALUE; private static ExecutorService reachabilityService; - private static final ObjectProperty globalEdgeStatus = new SimpleObjectProperty<>(EdgeStatus.INPUT); - // View stuff public StackPane root; public BorderPane borderPane; - public StackPane canvasPane; public StackPane topPane; public StackPane leftPane; public StackPane rightPane; public Rectangle bottomFillerElement; - public QueryPanePresentation queryPane; - public ProjectPanePresentation filePane; - public HBox toolbar; - public MessageTabPanePresentation messageTabPane; - public StackPane dialogContainer; - public JFXDialog dialog; + public StackPane modellingHelpDialogContainer; + public JFXDialog modellingHelpDialog; public StackPane modalBar; public JFXTextField queryTextField; public JFXTextField commentTextField; - public JFXRippler colorSelected; - public JFXRippler deleteSelected; - public JFXRippler undo; - public JFXRippler redo; public ImageView helpInitialImage; public StackPane helpInitialPane; @@ -89,10 +75,6 @@ public class EcdarController implements Initializable { public ImageView helpOutputImage; public StackPane helpOutputPane; - public JFXButton switchToInputButton; - public JFXButton switchToOutputButton; - public JFXToggleButton switchEdgeStatusButton; - public MenuItem menuEditMoveLeft; public MenuItem menuEditMoveUp; public MenuItem menuEditMoveRight; @@ -106,10 +88,10 @@ public class EcdarController implements Initializable { // The program top menu public MenuBar menuBar; - public MenuItem menuBarViewFilePanel; + public MenuItem menuBarViewProjectPanel; public MenuItem menuBarViewQueryPanel; public MenuItem menuBarViewGrid; - public MenuItem menuBarAutoscaling; + public MenuItem menuBarViewAutoscaling; public Menu menuViewMenuScaling; public ToggleGroup scaling; public RadioMenuItem scaleXS; @@ -120,6 +102,8 @@ public class EcdarController implements Initializable { public RadioMenuItem scaleXXL; public RadioMenuItem scaleXXXL; public MenuItem menuBarViewCanvasSplit; + public MenuItem menuBarViewEditor; + public MenuItem menuBarViewSimulator; public MenuItem menuBarFileCreateNewProject; public MenuItem menuBarFileOpenProject; public Menu menuBarFileRecentProjects; @@ -135,6 +119,7 @@ public class EcdarController implements Initializable { public MenuItem menuBarHelpAbout; public MenuItem menuBarHelpTest; + public JFXToggleButton switchGuiView; public Snackbar snackbar; public HBox statusBar; public Label statusLabel; @@ -148,6 +133,10 @@ public class EcdarController implements Initializable { public StackPane backendOptionsDialogContainer; public BackendOptionsDialogPresentation backendOptionsDialog; + + public StackPane simulationInitializationDialogContainer; + public SimulationInitializationDialogPresentation simulationInitializationDialog; + public final DoubleProperty scalingProperty = new SimpleDoubleProperty(); private static JFXDialog _queryDialog; @@ -161,10 +150,86 @@ public static void runReachabilityAnalysis() { reachabilityTime = System.currentTimeMillis() + 500; } - private static final ObjectProperty activeCanvasPresentation = new SimpleObjectProperty<>(new CanvasPresentation()); + /** + * Enumeration to keep track of which mode the application is in + */ + private enum Mode { + Editor, Simulator + } + + /** + * currentMode is a property that keeps track of which mode the application is in. + * The initial mode is Mode.Editor + */ + private static final ObjectProperty currentMode = new SimpleObjectProperty<>(Mode.Editor); + + private static final EditorPresentation editorPresentation = new EditorPresentation(); + public final ProjectPanePresentation projectPane = new ProjectPanePresentation(); + public final QueryPanePresentation queryPane = new QueryPanePresentation(); + + private static final SimulatorPresentation simulatorPresentation = new SimulatorPresentation(); + public final LeftSimPanePresentation leftSimPane = new LeftSimPanePresentation(); + public final RightSimPanePresentation rightSimPane = new RightSimPanePresentation(); + + @Override + public void initialize(final URL location, final ResourceBundle resources) { + initializeDialogs(); + initializeKeybindings(); + initializeStatusBar(); + initializeMenuBar(); + startBackgroundQueriesThread(); // Will terminate immediately if background queries are turned off + + // Update file coloring when active model changes + editorPresentation.getController().getActiveCanvasPresentation().getController().activeComponentProperty().addListener(observable -> projectPane.getController().updateColorsOnFilePresentations()); + editorPresentation.getController().activeCanvasPresentationProperty().addListener(observable -> projectPane.getController().updateColorsOnFilePresentations()); + + leftSimPane.minWidthProperty().bind(projectPane.minWidthProperty()); + leftSimPane.maxWidthProperty().bind(projectPane.maxWidthProperty()); + rightSimPane.minWidthProperty().bind(queryPane.minWidthProperty()); + rightSimPane.maxWidthProperty().bind(queryPane.maxWidthProperty()); + + enterEditorMode(); + } + + public StackPane getCenter() { + if (currentMode.get().equals(Mode.Editor)) { + return editorPresentation.getController().canvasPane; + } else { + return simulatorPresentation; + } + } public static EdgeStatus getGlobalEdgeStatus() { - return globalEdgeStatus.get(); + return editorPresentation.getController().getGlobalEdgeStatus(); + } + + public EditorPresentation getEditorPresentation() { + return editorPresentation; + } + + public SimulatorPresentation getSimulatorPresentation() { + return simulatorPresentation; + } + + public static CanvasPresentation getActiveCanvasPresentation() { + return editorPresentation.getController().getActiveCanvasPresentation(); + } + + public static DoubleProperty getActiveCanvasZoomFactor() { + return getActiveCanvasPresentation().getController().zoomHelper.currentZoomFactor; + } + + public static void setActiveCanvasPresentation(CanvasPresentation newActiveCanvasPresentation) { + getActiveCanvasPresentation().setOpacity(0.75); + newActiveCanvasPresentation.setOpacity(1); + editorPresentation.getController().setActiveCanvasPresentation(newActiveCanvasPresentation); + } + + public static void setActiveModelForActiveCanvas(HighLevelModelObject newActiveModel) { + getActiveCanvasPresentation().getController().setActiveModel(newActiveModel); + + // Change zoom level to fit new active model + Platform.runLater(() -> getActiveCanvasPresentation().getController().zoomHelper.zoomToFit()); } public static void setTemporaryComponentWatermarkVisibility(boolean visibility) { @@ -195,57 +260,55 @@ private void scaleIcons(Node node, double size) { Set xSmallIcons = node.lookupAll(".icon-size-x-small"); for (Node icon : xSmallIcons) icon.setStyle("-fx-icon-size: " + Math.floor(size / 13.0 * 18) + "px;"); + + switchGuiView.setSize(Math.floor(size / 13.0 * 24)); } private double getNewCalculatedScale() { return (Double.parseDouble(scaling.getSelectedToggle().getProperties().get("scale").toString()) * Ecdar.getDpiScale()) * 13.0; } - private void scaleEdgeStatusToggle(double size) { - switchEdgeStatusButton.setScaleX(size / 13.0); - switchEdgeStatusButton.setScaleY(size / 13.0); - } - - @Override - public void initialize(final URL location, final ResourceBundle resources) { - initilizeDialogs(); - initializeCanvasPane(); - initializeEdgeStatusHandling(); - initializeKeybindings(); - initializeStatusBar(); - initializeMenuBar(); - intitializeTemporaryComponentWatermark(); - startBackgroundQueriesThread(); // Will terminate immediately if background queries are turned off - - bottomFillerElement.heightProperty().bind(messageTabPane.maxHeightProperty()); - messageTabPane.getController().setRunnableForOpeningAndClosingMessageTabPane(this::changeInsetsOfFileAndQueryPanes); - } - - private void initilizeDialogs() { - dialog.setDialogContainer(dialogContainer); - dialogContainer.opacityProperty().bind(dialog.getChildren().get(0).scaleXProperty()); - dialog.setOnDialogClosed(event -> dialogContainer.setVisible(false)); + private void initializeDialogs() { + modellingHelpDialog.setDialogContainer(modellingHelpDialogContainer); + modellingHelpDialogContainer.opacityProperty().bind(modellingHelpDialog.getChildren().get(0).scaleXProperty()); + modellingHelpDialog.setOnDialogClosed(event -> modellingHelpDialogContainer.setVisible(false)); _queryDialog = queryDialog; _queryTextResult = queryTextResult; _queryTextQuery = queryTextQuery; initializeDialog(queryDialog, queryDialogContainer); - initializeDialog(backendOptionsDialog, backendOptionsDialogContainer); + initializeBackendOptionsDialog(); + initializeDialog(simulationInitializationDialog, simulationInitializationDialogContainer); + + simulationInitializationDialog.getController().cancelButton.setOnMouseClicked(event -> { + switchGuiView.setSelected(false); + simulationInitializationDialog.close(); + }); + + simulationInitializationDialog.getController().startButton.setOnMouseClicked(event -> { + + // ToDo NIELS: Start simulation of selected query + Ecdar.getSimulationHandler().setComposition(simulationInitializationDialog.getController().simulationComboBox.getSelectionModel().getSelectedItem()); + currentMode.setValue(Mode.Simulator); + simulationInitializationDialog.close(); + }); + } + + private void initializeBackendOptionsDialog() { + initializeDialog(backendOptionsDialog, backendOptionsDialogContainer); backendOptionsDialog.getController().resetBackendsButton.setOnMouseClicked(event -> { backendOptionsDialog.getController().resetBackendsToDefault(); }); backendOptionsDialog.getController().closeButton.setOnMouseClicked(event -> { backendOptionsDialog.getController().cancelBackendOptionsChanges(); - dialog.close(); backendOptionsDialog.close(); }); backendOptionsDialog.getController().saveButton.setOnMouseClicked(event -> { if (backendOptionsDialog.getController().saveChangesToBackendOptions()) { - dialog.close(); backendOptionsDialog.close(); } }); @@ -271,10 +334,9 @@ private void initializeDialog(JFXDialog dialog, StackPane dialogContainer) { dialogContainer.setMouseTransparent(false); }); - filePane.getStyleClass().add("responsive-pane-sizing"); + projectPane.getStyleClass().add("responsive-pane-sizing"); queryPane.getStyleClass().add("responsive-pane-sizing"); - initializeEdgeStatusHandling(); initializeKeybindings(); initializeStatusBar(); } @@ -319,17 +381,14 @@ private void initializeKeybindings() { event.consume(); nudgeSelected(NudgeDirection.UP); })); - KeyboardTracker.registerKeybind(KeyboardTracker.NUDGE_DOWN, new Keybind(new KeyCodeCombination(KeyCode.DOWN), (event) -> { event.consume(); nudgeSelected(NudgeDirection.DOWN); })); - KeyboardTracker.registerKeybind(KeyboardTracker.NUDGE_LEFT, new Keybind(new KeyCodeCombination(KeyCode.LEFT), (event) -> { event.consume(); nudgeSelected(NudgeDirection.LEFT); })); - KeyboardTracker.registerKeybind(KeyboardTracker.NUDGE_RIGHT, new Keybind(new KeyCodeCombination(KeyCode.RIGHT), (event) -> { event.consume(); nudgeSelected(NudgeDirection.RIGHT); @@ -343,67 +402,6 @@ private void initializeKeybindings() { KeyboardTracker.registerKeybind(KeyboardTracker.NUDGE_A, new Keybind(new KeyCodeCombination(KeyCode.A), () -> nudgeSelected(NudgeDirection.LEFT))); KeyboardTracker.registerKeybind(KeyboardTracker.NUDGE_S, new Keybind(new KeyCodeCombination(KeyCode.S), () -> nudgeSelected(NudgeDirection.DOWN))); KeyboardTracker.registerKeybind(KeyboardTracker.NUDGE_D, new Keybind(new KeyCodeCombination(KeyCode.D), () -> nudgeSelected(NudgeDirection.RIGHT))); - - // Keybind for deleting the selected elements - KeyboardTracker.registerKeybind(KeyboardTracker.DELETE_SELECTED, new Keybind(new KeyCodeCombination(KeyCode.DELETE), this::deleteSelectedClicked)); - - // Keybinds for coloring the selected elements - EnabledColor.enabledColors.forEach(enabledColor -> { - KeyboardTracker.registerKeybind(KeyboardTracker.COLOR_SELECTED + "_" + enabledColor.keyCode.getName(), new Keybind(new KeyCodeCombination(enabledColor.keyCode), () -> { - - final List> previousColor = new ArrayList<>(); - - SelectHelper.getSelectedElements().forEach(selectable -> { - previousColor.add(new Pair<>(selectable, new EnabledColor(selectable.getColor(), selectable.getColorIntensity()))); - }); - changeColorOnSelectedElements(enabledColor, previousColor); - SelectHelper.clearSelectedElements(); - })); - }); - } - - /** - * Handles the change of color on selected objects - * - * @param enabledColor The new color for the selected objects - * @param previousColor The color old color of the selected objects - */ - public void changeColorOnSelectedElements(final EnabledColor enabledColor, - final List> previousColor) { - UndoRedoStack.pushAndPerform(() -> { // Perform - SelectHelper.getSelectedElements() - .forEach(selectable -> selectable.color(enabledColor.color, enabledColor.intensity)); - }, () -> { // Undo - previousColor.forEach(selectableEnabledColorPair -> selectableEnabledColorPair.getKey().color(selectableEnabledColorPair.getValue().color, selectableEnabledColorPair.getValue().intensity)); - }, String.format("Changed the color of %d elements to %s", previousColor.size(), enabledColor.color.name()), "color-lens"); - } - - /** - * Initializes edge status. - * Input is the default status. - * This method sets buttons for edge status whenever the status changes. - */ - private void initializeEdgeStatusHandling() { - globalEdgeStatus.set(EdgeStatus.INPUT); - - Tooltip.install(switchToInputButton, new Tooltip("Switch to input mode")); - Tooltip.install(switchToOutputButton, new Tooltip("Switch to output mode")); - switchToInputButton.setDisableVisualFocus(true); // Hiding input button rippler on start-up - - globalEdgeStatus.addListener(((observable, oldValue, newValue) -> { - if (newValue.equals(EdgeStatus.INPUT)) { - switchToInputButton.setTextFill(javafx.scene.paint.Color.WHITE); - switchToOutputButton.setTextFill(javafx.scene.paint.Color.GREY); - switchEdgeStatusButton.setSelected(false); - } else { - switchToInputButton.setTextFill(javafx.scene.paint.Color.GREY); - switchToOutputButton.setTextFill(javafx.scene.paint.Color.WHITE); - switchEdgeStatusButton.setSelected(true); - } - })); - - // Ensure that the rippler is centered when scale is changed - Platform.runLater(() -> ((JFXRippler) switchEdgeStatusButton.lookup(".jfx-rippler")).setRipplerRecenter(true)); } private void startBackgroundQueriesThread() { @@ -440,7 +438,7 @@ private void startBackgroundQueriesThread() { if (!Ecdar.shouldRunBackgroundQueries.get()) return; Ecdar.getProject().getQueries().forEach(query -> { - if (query.isPeriodic()) query.run(); + if (query.isPeriodic()) Ecdar.getQueryExecutor().executeQuery(query); }); // List of threads to start @@ -453,42 +451,14 @@ private void startBackgroundQueriesThread() { component.getLocations().forEach(location -> location.setReachability(Location.Reachability.EXCLUDED)); } else { component.getLocations().forEach(location -> { - final String locationReachableQuery = BackendHelper.getLocationReachableQuery(location, component); + final String locationReachableQuery = BackendHelper.getLocationReachableQuery(location, component, SimulatorController.getSimulationQuery()); Query reachabilityQuery = new Query(locationReachableQuery, "", QueryState.UNKNOWN); reachabilityQuery.setType(QueryType.REACHABILITY); - Ecdar.getBackendDriver().addQueryToExecutionQueue(locationReachableQuery, - BackendHelper.getDefaultBackendInstance(), - (result -> { - if (result) { - location.setReachability(Location.Reachability.REACHABLE); - } else { - location.setReachability(Location.Reachability.UNREACHABLE); - } - Debug.removeThread(Thread.currentThread()); - }), - (e) -> { - location.setReachability(Location.Reachability.UNKNOWN); - Debug.removeThread(Thread.currentThread()); - }, - new QueryListener(reachabilityQuery)); - - final Thread verifyThread = new Thread(() -> Ecdar.getBackendDriver().addQueryToExecutionQueue(locationReachableQuery, - BackendHelper.getDefaultBackendInstance(), - (result -> { - if (result) { - location.setReachability(Location.Reachability.REACHABLE); - } else { - location.setReachability(Location.Reachability.UNREACHABLE); - } - Debug.removeThread(Thread.currentThread()); - }), - (e) -> { - location.setReachability(Location.Reachability.UNKNOWN); - Debug.removeThread(Thread.currentThread()); - }, - new QueryListener(reachabilityQuery))); + Ecdar.getQueryExecutor().executeQuery(reachabilityQuery); + + final Thread verifyThread = new Thread(() -> Ecdar.getQueryExecutor().executeQuery(reachabilityQuery)); verifyThread.setName(locationReachableQuery + " (" + verifyThread.getName() + ")"); Debug.addThread(verifyThread); @@ -554,27 +524,6 @@ private void initializeMenuBar() { initializeHelpMenu(); } - public static CanvasPresentation getActiveCanvasPresentation() { - return activeCanvasPresentation.get(); - } - - public static DoubleProperty getActiveCanvasZoomFactor() { - return getActiveCanvasPresentation().getController().zoomHelper.currentZoomFactor; - } - - public static void setActiveCanvasPresentation(CanvasPresentation newActiveCanvasPresentation) { - activeCanvasPresentation.get().setOpacity(0.75); - newActiveCanvasPresentation.setOpacity(1); - activeCanvasPresentation.set(newActiveCanvasPresentation); - } - - public static void setActiveModelForActiveCanvas(HighLevelModelObject newActiveModel) { - EcdarController.getActiveCanvasPresentation().getController().setActiveModel(newActiveModel); - - // Change zoom level to fit new active model - Platform.runLater(() -> EcdarController.getActiveCanvasPresentation().getController().zoomHelper.zoomToFit()); - } - private void initializeHelpMenu() { menuBarHelpHelp.setOnAction(event -> Ecdar.showHelp()); @@ -591,7 +540,6 @@ private void initializeHelpMenu() { }); aboutAcceptButton.setOnAction(event -> aboutDialog.close()); aboutDialog.setOnDialogClosed(event -> aboutContainer.setVisible(false)); // hide container when dialog is fully closed - } /** @@ -660,14 +608,14 @@ private void initializeEditMenu() { * Initialize the View menu. */ private void initializeViewMenu() { - menuBarViewFilePanel.getGraphic().setOpacity(1); - menuBarViewFilePanel.setAccelerator(new KeyCodeCombination(KeyCode.P, KeyCodeCombination.SHORTCUT_DOWN)); - menuBarViewFilePanel.setOnAction(event -> { - final BooleanProperty isOpen = Ecdar.toggleFilePane(); - menuBarViewFilePanel.getGraphic().opacityProperty().bind(new When(isOpen).then(1).otherwise(0)); + menuBarViewProjectPanel.getGraphic().setOpacity(1); + menuBarViewProjectPanel.setAccelerator(new KeyCodeCombination(KeyCode.P, KeyCodeCombination.SHORTCUT_DOWN)); + menuBarViewProjectPanel.setOnAction(event -> { + final BooleanProperty isOpen = Ecdar.toggleLeftPane(); + menuBarViewProjectPanel.getGraphic().opacityProperty().bind(new When(isOpen).then(1).otherwise(0)); }); - menuBarViewQueryPanel.getGraphic().setOpacity(0); + menuBarViewQueryPanel.getGraphic().setOpacity(1); menuBarViewQueryPanel.setAccelerator(new KeyCodeCombination(KeyCode.G, KeyCodeCombination.SHORTCUT_DOWN)); menuBarViewQueryPanel.setOnAction(event -> { final BooleanProperty isOpen = Ecdar.toggleQueryPane(); @@ -681,14 +629,14 @@ private void initializeViewMenu() { menuBarViewGrid.getGraphic().opacityProperty().bind(new When(isOn).then(1).otherwise(0)); }); - menuBarAutoscaling.getGraphic().setOpacity(Ecdar.autoScalingEnabled.getValue() ? 1 : 0); - menuBarAutoscaling.setOnAction(event -> { + menuBarViewAutoscaling.getGraphic().setOpacity(Ecdar.autoScalingEnabled.getValue() ? 1 : 0); + menuBarViewAutoscaling.setOnAction(event -> { Ecdar.autoScalingEnabled.setValue(!Ecdar.autoScalingEnabled.getValue()); updateScaling(getNewCalculatedScale() / 13); Ecdar.preferences.put("autoscaling", String.valueOf(Ecdar.autoScalingEnabled.getValue())); }); Ecdar.autoScalingEnabled.addListener((observable, oldValue, newValue) -> { - menuBarAutoscaling.getGraphic().opacityProperty().setValue(newValue ? 1 : 0); + menuBarViewAutoscaling.getGraphic().opacityProperty().setValue(newValue ? 1 : 0); }); scaling.selectedToggleProperty().addListener((observable, oldValue, newValue) -> updateScaling(Double.parseDouble(newValue.getProperties().get("scale").toString()))); @@ -697,14 +645,54 @@ private void initializeViewMenu() { menuBarViewCanvasSplit.setOnAction(event -> { final BooleanProperty isSplit = Ecdar.toggleCanvasSplit(); if (isSplit.get()) { - Platform.runLater(this::setCanvasModeToSingular); + Platform.runLater(() -> editorPresentation.getController().setCanvasModeToSingular()); menuBarViewCanvasSplit.setText("Split canvas"); } else { - Platform.runLater(this::setCanvasModeToSplit); + Platform.runLater(() -> editorPresentation.getController().setCanvasModeToSplit()); menuBarViewCanvasSplit.setText("Merge canvases"); } }); + switchGuiView.selectedProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) { + if (Ecdar.getProject().getQueries().isEmpty()) { + Ecdar.showToast("Please add a query to simulate before entering the simulator"); + switchGuiView.setSelected(false); + return; + } + + if (!Ecdar.getSimulationHandler().isSimulationRunning()) { + ArrayList queryOptions = Ecdar.getProject().getQueries().stream().map(Query::getQuery).collect(Collectors.toCollection(ArrayList::new)); + if (!simulationInitializationDialog.getController().simulationComboBox.getItems().equals(queryOptions)) { + simulationInitializationDialog.getController().simulationComboBox.getItems().setAll(queryOptions); + } + + simulationInitializationDialogContainer.setVisible(true); + simulationInitializationDialog.show(simulationInitializationDialogContainer); + simulationInitializationDialog.setMouseTransparent(false); + } else { + currentMode.setValue(Mode.Simulator); + } + } else { + currentMode.setValue(Mode.Editor); + } + }); + + currentMode.addListener((obs, oldMode, newMode) -> { + if (newMode == Mode.Editor && oldMode != newMode) { + enterEditorMode(); + } else if (newMode == Mode.Simulator && oldMode != newMode) { + enterSimulatorMode(); + } + }); + + menuBarViewEditor.setAccelerator(new KeyCodeCombination(KeyCode.DIGIT1, KeyCombination.SHORTCUT_DOWN)); + menuBarViewEditor.setOnAction(event -> switchGuiView.setSelected(false)); + + menuBarViewSimulator.setAccelerator(new KeyCodeCombination(KeyCode.DIGIT2, KeyCombination.SHORTCUT_DOWN)); + menuBarViewSimulator.setOnAction(event -> switchGuiView.setSelected(true)); + + // On startup, set the scaling to the values saved in preferences Platform.runLater(() -> { Ecdar.autoScalingEnabled.setValue(Ecdar.preferences.getBoolean("autoscaling", true)); @@ -733,8 +721,7 @@ private void updateScaling(double newScale) { Ecdar.preferences.put("scale", String.valueOf(newScale)); scaleIcons(root, newCalculatedScale); - scaleEdgeStatusToggle(newCalculatedScale); - messageTabPane.getController().updateScale(newScale); + editorPresentation.getController().scaleEdgeStatusToggle(newCalculatedScale); // Update listeners of UI scale scalingProperty.set(newScale); @@ -999,152 +986,43 @@ private void initializeFileExportAsPng() { }); } - private void initializeCanvasPane() { - Platform.runLater(this::setCanvasModeToSingular); - } - - /** - * Removes the canvases and adds a new one, with the active component of the active canvasPresentation. - */ - private void setCanvasModeToSingular() { - canvasPane.getChildren().clear(); - CanvasPresentation canvasPresentation = new CanvasPresentation(); - HighLevelModelObject model = activeCanvasPresentation.get().getController().getActiveModel(); - if (model != null) { - canvasPresentation.getController().setActiveModel(activeCanvasPresentation.get().getController().getActiveModel()); - } else { - // If no components where found, the project has not been initialized. The active model will be updated when the project is initialized - canvasPresentation.getController().setActiveModel(Ecdar.getProject().getComponents().stream().findFirst().orElse(null)); - } - - canvasPane.getChildren().add(canvasPresentation); - activeCanvasPresentation.set(canvasPresentation); - filePane.getController().updateColorsOnFilePresentations(); - - Rectangle clip = new Rectangle(); - clip.setArcWidth(1); - clip.setArcHeight(1); - clip.widthProperty().bind(canvasPane.widthProperty()); - clip.heightProperty().bind(canvasPane.heightProperty()); - canvasPresentation.getController().zoomablePane.setClip(clip); - - canvasPresentation.getController().zoomablePane.minWidthProperty().bind(canvasPane.widthProperty()); - canvasPresentation.getController().zoomablePane.maxWidthProperty().bind(canvasPane.widthProperty()); - canvasPresentation.getController().zoomablePane.minHeightProperty().bind(canvasPane.heightProperty()); - canvasPresentation.getController().zoomablePane.maxHeightProperty().bind(canvasPane.heightProperty()); - } - /** - * Removes the canvas and adds a GridPane with four new canvases, with different active components, - * the first being the one previously displayed on the single canvas. + * Changes the view and mode to the editor + * Only enter if the mode is not already Editor */ - private void setCanvasModeToSplit() { - canvasPane.getChildren().clear(); - - GridPane canvasGrid = new GridPane(); - - canvasGrid.addColumn(0); - canvasGrid.addColumn(1); - canvasGrid.addRow(0); - canvasGrid.addRow(1); - - ColumnConstraints col1 = new ColumnConstraints(); - col1.setPercentWidth(50); - - RowConstraints row1 = new RowConstraints(); - row1.setPercentHeight(50); - - canvasGrid.getColumnConstraints().add(col1); - canvasGrid.getColumnConstraints().add(col1); - canvasGrid.getRowConstraints().add(row1); - canvasGrid.getRowConstraints().add(row1); - - ObservableList components = Ecdar.getProject().getComponents(); - int currentCompNum = 0, numComponents = components.size(); - - // Add the canvasPresentation at the top-left - CanvasPresentation canvasPresentation = initializeNewCanvasPresentation(); - canvasPresentation.getController().setActiveModel(getActiveCanvasPresentation().getController().getActiveModel()); - canvasGrid.add(canvasPresentation, 0, 0); - setActiveCanvasPresentation(canvasPresentation); - - // Add the canvasPresentation at the top-right - canvasPresentation = initializeNewCanvasPresentationWithActiveComponent(components, currentCompNum); - canvasPresentation.setOpacity(0.75); - canvasGrid.add(canvasPresentation, 1, 0); - // Update the startIndex for the next canvasPresentation - for (int i = 0; i < numComponents; i++) { - if (canvasPresentation.getController().getActiveModel() != null && canvasPresentation.getController().getActiveModel().equals(components.get(i))) { - currentCompNum = i + 1; - } - } - - // Add the canvasPresentation at the bottom-left - canvasPresentation = initializeNewCanvasPresentationWithActiveComponent(components, currentCompNum); - canvasPresentation.setOpacity(0.75); - canvasGrid.add(canvasPresentation, 0, 1); - - // Update the startIndex for the next canvasPresentation - for (int i = 0; i < numComponents; i++) - if (canvasPresentation.getController().getActiveModel() != null && canvasPresentation.getController().getActiveModel().equals(components.get(i))) { - currentCompNum = i + 1; - } - - // Add the canvasPresentation at the bottom-right - canvasPresentation = initializeNewCanvasPresentationWithActiveComponent(components, currentCompNum); - canvasPresentation.setOpacity(0.75); - canvasGrid.add(canvasPresentation, 1, 1); - - canvasPane.getChildren().add(canvasGrid); - filePane.getController().updateColorsOnFilePresentations(); + private void enterEditorMode() { +// ToDo NIELS: Consider implementing willShow and willHide to handle general elements that should only be available for one of the modes +// editorPresentation.getController().willShow(); +// simulatorPresentation.getController().willHide(); + + borderPane.setCenter(editorPresentation); + leftPane.getChildren().clear(); + leftPane.getChildren().add(projectPane); + rightPane.getChildren().clear(); + rightPane.getChildren().add(queryPane); + scaling.selectToggle(scaleM); // temporary fix for scaling issue + + // Enable or disable the menu items that can be used when in the simulator +// updateMenuItems(); } /** - * Initialize a new CanvasShellPresentation and set its active component to the next component encountered from the startIndex and return it - * - * @param components the list of components for assigning active component of the CanvasPresentation - * @param startIndex the index to start at when trying to find the component to set as active - * @return new CanvasShellPresentation + * Changes the view and mode to the simulator + * Only enter if the mode is not already Simulator */ - private CanvasPresentation initializeNewCanvasPresentationWithActiveComponent(ObservableList components, int startIndex) { - CanvasPresentation canvasPresentation = initializeNewCanvasPresentation(); - - int numComponents = components.size(); - canvasPresentation.getController().setActiveModel(null); - for (int currentCompNum = startIndex; currentCompNum < numComponents; currentCompNum++) { - if (getActiveCanvasPresentation().getController().getActiveModel() != components.get(currentCompNum)) { - canvasPresentation.getController().setActiveModel(components.get(currentCompNum)); - break; - } - } - - return canvasPresentation; - } - - /** - * Initialize a new CanvasPresentation and return it - * - * @return new CanvasPresentation - */ - private CanvasPresentation initializeNewCanvasPresentation() { - CanvasPresentation canvasPresentation = new CanvasPresentation(); - canvasPresentation.setBorder(new Border(new BorderStroke(Color.GREY.getColor(Color.Intensity.I500), BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderStroke.THIN))); - - // Set th clip of the zoomable pane to be half of the canvasPane, - // to ensure a 2 by 2 grid without overflowing borders - Rectangle clip = new Rectangle(); - clip.setArcWidth(1); - clip.setArcHeight(1); - clip.widthProperty().bind(canvasPane.widthProperty().divide(2)); - clip.heightProperty().bind(canvasPane.heightProperty().divide(2)); - canvasPresentation.getController().zoomablePane.setClip(clip); - - canvasPresentation.getController().zoomablePane.minWidthProperty().bind(canvasPane.widthProperty().divide(2)); - canvasPresentation.getController().zoomablePane.maxWidthProperty().bind(canvasPane.widthProperty().divide(2)); - canvasPresentation.getController().zoomablePane.minHeightProperty().bind(canvasPane.heightProperty().divide(2)); - canvasPresentation.getController().zoomablePane.maxHeightProperty().bind(canvasPane.heightProperty().divide(2)); - - return canvasPresentation; + private void enterSimulatorMode() { +// ToDo NIELS: Consider implementing willShow and willHide to handle general elements that should only be available for one of the modes +// ecdarPresentation.getController().willHide(); + simulatorPresentation.getController().willShow(); + + borderPane.setCenter(simulatorPresentation); + leftPane.getChildren().clear(); + leftPane.getChildren().add(leftSimPane); + rightPane.getChildren().clear(); + rightPane.getChildren().add(rightSimPane); + + // Enable or disable the menu items that can be used when in the simulator +// updateMenuItems(); } /** @@ -1308,20 +1186,6 @@ private static int getAutoCropRightX(final BufferedImage image) { throw new IllegalArgumentException("Image is all white"); } - /** - * This method is used to push the contents of the file and query panes when the tab pane is opened - */ - private void changeInsetsOfFileAndQueryPanes() { - if (messageTabPane.getController().isOpen()) { - filePane.showBottomInset(false); - queryPane.showBottomInset(false); - CanvasPresentation.showBottomInset(false); - } else { - filePane.showBottomInset(true); - queryPane.showBottomInset(true); - CanvasPresentation.showBottomInset(true); - } - } private void nudgeSelected(final NudgeDirection direction) { final List selectedElements = SelectHelper.getSelectedElements(); @@ -1355,88 +1219,9 @@ private void nudgeSelected(final NudgeDirection direction) { "open-with"); } - @FXML - private void deleteSelectedClicked() { - if (SelectHelper.getSelectedElements().size() == 0) return; - - // Run through the selected elements and look for something that we can delete - SelectHelper.getSelectedElements().forEach(selectable -> { - if (selectable instanceof LocationController) { - ((LocationController) selectable).tryDelete(); - } else if (selectable instanceof EdgeController) { - final Component component = ((EdgeController) selectable).getComponent(); - final DisplayableEdge edge = ((EdgeController) selectable).getEdge(); - - // Dont delete edge if it is locked - if (edge.getIsLockedProperty().getValue()) { - return; - } - - UndoRedoStack.pushAndPerform(() -> { // Perform - // Remove the edge - component.removeEdge(edge); - }, () -> { // Undo - // Re-all the edge - component.addEdge(edge); - }, String.format("Deleted %s", selectable.toString()), "delete"); - } else if (selectable instanceof NailController) { - ((NailController) selectable).tryDelete(); - } - }); - - SelectHelper.clearSelectedElements(); - } - - @FXML - private void undoClicked() { - UndoRedoStack.undo(); - } - - @FXML - private void redoClicked() { - UndoRedoStack.redo(); - } - - /** - * Switch to input edge mode - */ - @FXML - private void switchToInputClicked() { - setGlobalEdgeStatus(EdgeStatus.INPUT); - } - - /** - * Switch to output edge mode - */ - @FXML - private void switchToOutputClicked() { - setGlobalEdgeStatus(EdgeStatus.OUTPUT); - } - - /** - * Switch edge status. - */ - @FXML - private void switchEdgeStatusClicked() { - if (getGlobalEdgeStatus().equals(EdgeStatus.INPUT)) { - setGlobalEdgeStatus(EdgeStatus.OUTPUT); - } else { - setGlobalEdgeStatus(EdgeStatus.INPUT); - } - } - - /** - * Sets the global edge status. - * - * @param status the status - */ - private void setGlobalEdgeStatus(EdgeStatus status) { - globalEdgeStatus.set(status); - } - @FXML private void closeQueryDialog() { - dialog.close(); + modellingHelpDialog.close(); queryDialog.close(); } diff --git a/src/main/java/ecdar/controllers/EdgeController.java b/src/main/java/ecdar/controllers/EdgeController.java index 3610934f..c2135c4c 100644 --- a/src/main/java/ecdar/controllers/EdgeController.java +++ b/src/main/java/ecdar/controllers/EdgeController.java @@ -73,7 +73,6 @@ public void initialize(final URL location, final ResourceBundle resources) { } }); }); - initializeLinksListener(); ensureNailsInFront(); initializeSelectListener(); diff --git a/src/main/java/ecdar/controllers/EditorController.java b/src/main/java/ecdar/controllers/EditorController.java new file mode 100644 index 00000000..27ab5245 --- /dev/null +++ b/src/main/java/ecdar/controllers/EditorController.java @@ -0,0 +1,380 @@ +package ecdar.controllers; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXRippler; +import com.jfoenix.controls.JFXToggleButton; +import ecdar.Ecdar; +import ecdar.abstractions.Component; +import ecdar.abstractions.DisplayableEdge; +import ecdar.abstractions.EdgeStatus; +import ecdar.abstractions.HighLevelModelObject; +import ecdar.presentations.CanvasPresentation; +import ecdar.utility.UndoRedoStack; +import ecdar.utility.colors.Color; +import ecdar.utility.colors.EnabledColor; +import ecdar.utility.helpers.SelectHelper; +import ecdar.utility.keyboard.Keybind; +import ecdar.utility.keyboard.KeyboardTracker; +import javafx.application.Platform; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Tooltip; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.layout.*; +import javafx.scene.shape.Rectangle; +import javafx.util.Pair; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; + +public class EditorController implements Initializable { + public VBox root; + public HBox toolbar; + public JFXRippler colorSelected; + public JFXRippler deleteSelected; + public JFXRippler undo; + public JFXRippler redo; + public JFXButton switchToInputButton; + public JFXButton switchToOutputButton; + public JFXToggleButton switchEdgeStatusButton; + public StackPane canvasPane; + + private final ObjectProperty globalEdgeStatus = new SimpleObjectProperty<>(EdgeStatus.INPUT); + private final ObjectProperty activeCanvasPresentation = new SimpleObjectProperty<>(new CanvasPresentation()); + + @Override + public void initialize(final URL location, final ResourceBundle resources) { + initializeCanvasPane(); + initializeEdgeStatusHandling(); + initializeKeybindings(); + } + + public EdgeStatus getGlobalEdgeStatus() { + return globalEdgeStatus.get(); + } + + ObjectProperty activeCanvasPresentationProperty() { + return activeCanvasPresentation; + } + + public CanvasPresentation getActiveCanvasPresentation() { + return activeCanvasPresentation.get(); + } + + public void setActiveCanvasPresentation(CanvasPresentation newActiveCanvasPresentation) { + activeCanvasPresentation.get().setOpacity(0.75); + newActiveCanvasPresentation.setOpacity(1); + activeCanvasPresentation.set(newActiveCanvasPresentation); + } + + public void setActiveModelForActiveCanvas(HighLevelModelObject newActiveModel) { + EcdarController.getActiveCanvasPresentation().getController().setActiveModel(newActiveModel); + + // Change zoom level to fit new active model + Platform.runLater(() -> EcdarController.getActiveCanvasPresentation().getController().zoomHelper.zoomToFit()); + } + + private void initializeCanvasPane() { + Platform.runLater(this::setCanvasModeToSingular); + } + + private void initializeKeybindings() { + // Keybinds for coloring the selected elements + EnabledColor.enabledColors.forEach(enabledColor -> { + KeyboardTracker.registerKeybind(KeyboardTracker.COLOR_SELECTED + "_" + enabledColor.keyCode.getName(), new Keybind(new KeyCodeCombination(enabledColor.keyCode), () -> { + + final List> previousColor = new ArrayList<>(); + + SelectHelper.getSelectedElements().forEach(selectable -> { + previousColor.add(new Pair<>(selectable, new EnabledColor(selectable.getColor(), selectable.getColorIntensity()))); + }); + changeColorOnSelectedElements(enabledColor, previousColor); + SelectHelper.clearSelectedElements(); + })); + }); + + // Keybind for deleting the selected elements + KeyboardTracker.registerKeybind(KeyboardTracker.DELETE_SELECTED, new Keybind(new KeyCodeCombination(KeyCode.DELETE), this::deleteSelectedClicked)); + } + + /** + * Initializes edge status. + * Input is the default status. + * This method sets buttons for edge status whenever the status changes. + */ + private void initializeEdgeStatusHandling() { + globalEdgeStatus.set(EdgeStatus.INPUT); + + Tooltip.install(switchToInputButton, new Tooltip("Switch to input mode")); + Tooltip.install(switchToOutputButton, new Tooltip("Switch to output mode")); + switchToInputButton.setDisableVisualFocus(true); // Hiding input button rippler on start-up + + globalEdgeStatus.addListener(((observable, oldValue, newValue) -> { + if (newValue.equals(EdgeStatus.INPUT)) { + switchToInputButton.setTextFill(javafx.scene.paint.Color.WHITE); + switchToOutputButton.setTextFill(javafx.scene.paint.Color.GREY); + switchEdgeStatusButton.setSelected(false); + } else { + switchToInputButton.setTextFill(javafx.scene.paint.Color.GREY); + switchToOutputButton.setTextFill(javafx.scene.paint.Color.WHITE); + switchEdgeStatusButton.setSelected(true); + } + })); + + // Ensure that the rippler is centered when scale is changed + Platform.runLater(() -> { + var rippler = ((JFXRippler) switchEdgeStatusButton.lookup(".jfx-rippler")); + if (rippler == null) return; + rippler.setRipplerRecenter(true); + }); + } + + /** + * Sets the global edge status. + * + * @param status the status + */ + private void setGlobalEdgeStatus(EdgeStatus status) { + globalEdgeStatus.set(status); + } + + void scaleEdgeStatusToggle(double size) { + switchEdgeStatusButton.setScaleX(size / 13.0); + switchEdgeStatusButton.setScaleY(size / 13.0); + } + + /** + * Handles the change of color on selected objects + * + * @param enabledColor The new color for the selected objects + * @param previousColor The old color of the selected objects + */ + public void changeColorOnSelectedElements(final EnabledColor enabledColor, + final List> previousColor) { + UndoRedoStack.pushAndPerform(() -> { // Perform + SelectHelper.getSelectedElements() + .forEach(selectable -> selectable.color(enabledColor.color, enabledColor.intensity)); + }, () -> { // Undo + previousColor.forEach(selectableEnabledColorPair -> selectableEnabledColorPair.getKey().color(selectableEnabledColorPair.getValue().color, selectableEnabledColorPair.getValue().intensity)); + }, String.format("Changed the color of %d elements to %s", previousColor.size(), enabledColor.color.name()), "color-lens"); + } + + /** + * Removes the canvases and adds a new one, with the active component of the active canvasPresentation. + */ + void setCanvasModeToSingular() { + canvasPane.getChildren().clear(); + CanvasPresentation canvasPresentation = new CanvasPresentation(); + HighLevelModelObject model = activeCanvasPresentation.get().getController().getActiveModel(); + if (model != null) { + canvasPresentation.getController().setActiveModel(activeCanvasPresentation.get().getController().getActiveModel()); + } else { + // If no components where found, the project has not been initialized. The active model will be updated when the project is initialized + canvasPresentation.getController().setActiveModel(Ecdar.getProject().getComponents().stream().findFirst().orElse(null)); + } + + canvasPane.getChildren().add(canvasPresentation); + activeCanvasPresentation.set(canvasPresentation); + + Rectangle clip = new Rectangle(); + clip.setArcWidth(1); + clip.setArcHeight(1); + clip.widthProperty().bind(canvasPane.widthProperty()); + clip.heightProperty().bind(canvasPane.heightProperty()); + + canvasPresentation.getController().zoomablePane.setClip(clip); + canvasPresentation.getController().zoomablePane.minWidthProperty().bind(canvasPane.widthProperty()); + canvasPresentation.getController().zoomablePane.maxWidthProperty().bind(canvasPane.widthProperty()); + canvasPresentation.getController().zoomablePane.minHeightProperty().bind(canvasPane.heightProperty()); + canvasPresentation.getController().zoomablePane.maxHeightProperty().bind(canvasPane.heightProperty()); + } + + /** + * Removes the canvas and adds a GridPane with four new canvases, with different active components, + * the first being the one previously displayed on the single canvas. + */ + void setCanvasModeToSplit() { + canvasPane.getChildren().clear(); + + GridPane canvasGrid = new GridPane(); + + canvasGrid.addColumn(0); + canvasGrid.addColumn(1); + canvasGrid.addRow(0); + canvasGrid.addRow(1); + + ColumnConstraints col1 = new ColumnConstraints(); + col1.setPercentWidth(50); + + RowConstraints row1 = new RowConstraints(); + row1.setPercentHeight(50); + + canvasGrid.getColumnConstraints().add(col1); + canvasGrid.getColumnConstraints().add(col1); + canvasGrid.getRowConstraints().add(row1); + canvasGrid.getRowConstraints().add(row1); + + ObservableList components = Ecdar.getProject().getComponents(); + int currentCompNum = 0, numComponents = components.size(); + + // Add the canvasPresentation at the top-left + CanvasPresentation canvasPresentation = initializeNewCanvasPresentation(); + canvasPresentation.getController().setActiveModel(getActiveCanvasPresentation().getController().getActiveModel()); + canvasGrid.add(canvasPresentation, 0, 0); + setActiveCanvasPresentation(canvasPresentation); + + // Add the canvasPresentation at the top-right + canvasPresentation = initializeNewCanvasPresentationWithActiveComponent(components, currentCompNum); + canvasPresentation.setOpacity(0.75); + canvasGrid.add(canvasPresentation, 1, 0); + + // Update the startIndex for the next canvasPresentation + for (int i = 0; i < numComponents; i++) { + if (canvasPresentation.getController().getActiveModel() != null && canvasPresentation.getController().getActiveModel().equals(components.get(i))) { + currentCompNum = i + 1; + } + } + + // Add the canvasPresentation at the bottom-left + canvasPresentation = initializeNewCanvasPresentationWithActiveComponent(components, currentCompNum); + canvasPresentation.setOpacity(0.75); + canvasGrid.add(canvasPresentation, 0, 1); + + // Update the startIndex for the next canvasPresentation + for (int i = 0; i < numComponents; i++) + if (canvasPresentation.getController().getActiveModel() != null && canvasPresentation.getController().getActiveModel().equals(components.get(i))) { + currentCompNum = i + 1; + } + + // Add the canvasPresentation at the bottom-right + canvasPresentation = initializeNewCanvasPresentationWithActiveComponent(components, currentCompNum); + canvasPresentation.setOpacity(0.75); + canvasGrid.add(canvasPresentation, 1, 1); + + canvasPane.getChildren().add(canvasGrid); + } + + /** + * Initialize a new CanvasShellPresentation and set its active component to the next component encountered from the startIndex and return it + * + * @param components the list of components for assigning active component of the CanvasPresentation + * @param startIndex the index to start at when trying to find the component to set as active + * @return new CanvasShellPresentation + */ + private CanvasPresentation initializeNewCanvasPresentationWithActiveComponent(ObservableList components, int startIndex) { + CanvasPresentation canvasPresentation = initializeNewCanvasPresentation(); + + int numComponents = components.size(); + canvasPresentation.getController().setActiveModel(null); + for (int currentCompNum = startIndex; currentCompNum < numComponents; currentCompNum++) { + if (getActiveCanvasPresentation().getController().getActiveModel() != components.get(currentCompNum)) { + canvasPresentation.getController().setActiveModel(components.get(currentCompNum)); + break; + } + } + + return canvasPresentation; + } + + /** + * Initialize a new CanvasPresentation and return it + * + * @return new CanvasPresentation + */ + private CanvasPresentation initializeNewCanvasPresentation() { + CanvasPresentation canvasPresentation = new CanvasPresentation(); + canvasPresentation.setBorder(new Border(new BorderStroke(Color.GREY.getColor(Color.Intensity.I500), BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderStroke.THIN))); + + // Set th clip of the zoomable pane to be half of the canvasPane, + // to ensure a 2 by 2 grid without overflowing borders + Rectangle clip = new Rectangle(); + clip.setArcWidth(1); + clip.setArcHeight(1); + clip.widthProperty().bind(canvasPane.widthProperty().divide(2)); + clip.heightProperty().bind(canvasPane.heightProperty().divide(2)); + canvasPresentation.getController().zoomablePane.setClip(clip); + + canvasPresentation.getController().zoomablePane.minWidthProperty().bind(canvasPane.widthProperty().divide(2)); + canvasPresentation.getController().zoomablePane.maxWidthProperty().bind(canvasPane.widthProperty().divide(2)); + canvasPresentation.getController().zoomablePane.minHeightProperty().bind(canvasPane.heightProperty().divide(2)); + canvasPresentation.getController().zoomablePane.maxHeightProperty().bind(canvasPane.heightProperty().divide(2)); + + return canvasPresentation; + } + + @FXML + private void undoClicked() { + UndoRedoStack.undo(); + } + + @FXML + private void redoClicked() { + UndoRedoStack.redo(); + } + + /** + * Switch to input edge mode + */ + @FXML + private void switchToInputClicked() { + setGlobalEdgeStatus(EdgeStatus.INPUT); + } + + /** + * Switch to output edge mode + */ + @FXML + private void switchToOutputClicked() { + setGlobalEdgeStatus(EdgeStatus.OUTPUT); + } + + /** + * Switch edge status. + */ + @FXML + private void switchEdgeStatusClicked() { + if (getGlobalEdgeStatus().equals(EdgeStatus.INPUT)) { + setGlobalEdgeStatus(EdgeStatus.OUTPUT); + } else { + setGlobalEdgeStatus(EdgeStatus.INPUT); + } + } + + @FXML + private void deleteSelectedClicked() { + if (SelectHelper.getSelectedElements().size() == 0) return; + + // Run through the selected elements and look for something that we can delete + SelectHelper.getSelectedElements().forEach(selectable -> { + if (selectable instanceof LocationController) { + ((LocationController) selectable).tryDelete(); + } else if (selectable instanceof EdgeController) { + final Component component = ((EdgeController) selectable).getComponent(); + final DisplayableEdge edge = ((EdgeController) selectable).getEdge(); + + // Dont delete edge if it is locked + if (edge.getIsLockedProperty().getValue()) { + return; + } + + UndoRedoStack.pushAndPerform(() -> { // Perform + // Remove the edge + component.removeEdge(edge); + }, () -> { // Undo + // Re-all the edge + component.addEdge(edge); + }, String.format("Deleted %s", selectable.toString()), "delete"); + } else if (selectable instanceof NailController) { + ((NailController) selectable).tryDelete(); + } + }); + + SelectHelper.clearSelectedElements(); + } +} diff --git a/src/main/java/ecdar/controllers/LeftSimPaneController.java b/src/main/java/ecdar/controllers/LeftSimPaneController.java new file mode 100755 index 00000000..7f68aaaa --- /dev/null +++ b/src/main/java/ecdar/controllers/LeftSimPaneController.java @@ -0,0 +1,24 @@ +package ecdar.controllers; + +import ecdar.presentations.TracePaneElementPresentation; +import ecdar.presentations.TransitionPaneElementPresentation; +import javafx.fxml.Initializable; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import java.net.URL; +import java.util.ResourceBundle; + +public class LeftSimPaneController implements Initializable { + public StackPane root; + public ScrollPane scrollPane; + public VBox scrollPaneVbox; + + public TransitionPaneElementPresentation transitionPanePresentation; + public TracePaneElementPresentation tracePanePresentation; + + @Override + public void initialize(URL location, ResourceBundle resources) { + + } +} diff --git a/src/main/java/ecdar/controllers/LocationController.java b/src/main/java/ecdar/controllers/LocationController.java index 64bf12d5..2b887f9e 100644 --- a/src/main/java/ecdar/controllers/LocationController.java +++ b/src/main/java/ecdar/controllers/LocationController.java @@ -9,7 +9,6 @@ import ecdar.utility.UndoRedoStack; import ecdar.utility.colors.Color; import ecdar.utility.helpers.ItemDragHelper; -import ecdar.utility.helpers.MouseCircular; import ecdar.utility.helpers.SelectHelper; import ecdar.utility.keyboard.Keybind; import ecdar.utility.keyboard.KeyboardTracker; @@ -194,24 +193,6 @@ public void initializeDropDownMenu() { dropDownMenu.addSpacerElement(); - dropDownMenu.addClickableListElement("Is " + getLocation().getId() + " reachable?", event -> { - dropDownMenu.hide(); - // Generate the query from the backend - final String reachabilityQuery = BackendHelper.getLocationReachableQuery(getLocation(), getComponent()); - - // Add proper comment - final String reachabilityComment = "Is " + getLocation().getMostDescriptiveIdentifier() + " reachable?"; - - // Add new query for this location - final Query query = new Query(reachabilityQuery, reachabilityComment, QueryState.UNKNOWN); - query.setType(QueryType.REACHABILITY); - Ecdar.getProject().getQueries().add(query); - query.run(); - dropDownMenu.hide(); - }); - - dropDownMenu.addSpacerElement(); - dropDownMenu.addColorPicker(getLocation(), (color, intensity) -> { getLocation().setColorIntensity(intensity); getLocation().setColor(color); diff --git a/src/main/java/ecdar/controllers/MessageTabPaneController.java b/src/main/java/ecdar/controllers/MessageTabPaneController.java deleted file mode 100644 index 022f582c..00000000 --- a/src/main/java/ecdar/controllers/MessageTabPaneController.java +++ /dev/null @@ -1,272 +0,0 @@ -package ecdar.controllers; - -import com.jfoenix.controls.JFXRippler; -import com.jfoenix.controls.JFXTabPane; -import ecdar.Ecdar; -import ecdar.abstractions.Component; -import ecdar.code_analysis.CodeAnalysis; -import ecdar.presentations.MessageCollectionPresentation; -import ecdar.presentations.MessagePresentation; -import javafx.animation.Interpolator; -import javafx.animation.Transition; -import javafx.application.Platform; -import javafx.beans.property.Property; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.collections.ListChangeListener; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.Tab; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; -import javafx.scene.shape.Rectangle; -import javafx.util.Duration; -import org.kordamp.ikonli.javafx.FontIcon; - -import java.net.URL; -import java.util.HashMap; -import java.util.Map; -import java.util.ResourceBundle; -import java.util.function.Consumer; - -public class MessageTabPaneController implements Initializable { - public StackPane root; - public JFXTabPane tabPane; - public Tab errorsTab; - public Tab warningsTab; - public Rectangle tabPaneResizeElement; - public StackPane tabPaneContainer; - public JFXRippler collapseMessages; - public FontIcon collapseMessagesIcon; - public ScrollPane errorsScrollPane; - public VBox errorsList; - public ScrollPane warningsScrollPane; - public VBox warningsList; - public Tab backendErrorsTab; - public ScrollPane backendErrorsScrollPane; - public VBox backendErrorsList; - - private double tabPanePreviousY = 0; - private double expandHeight = 300; - private double collapseHeight = 35; - private final Property isOpen = new SimpleBooleanProperty(false); - private Runnable openCloseExternalAction; - public boolean shouldISkipOpeningTheMessagesContainer = true; - - private final Transition expandMessagesContainer = new Transition() { - { - setInterpolator(Interpolator.SPLINE(0.645, 0.045, 0.355, 1)); - setCycleDuration(Duration.millis(200)); - } - - @Override - protected void interpolate(final double frac) { - tabPaneContainer.setMaxHeight(collapseHeight + frac * (expandHeight - collapseHeight)); - } - }; - private Transition collapseMessagesContainer; - - @Override - public void initialize(final URL location, final ResourceBundle resources) { - Platform.runLater(() -> { - collapseMessagesContainer = new Transition() { - { - setInterpolator(Interpolator.SPLINE(0.645, 0.045, 0.355, 1)); - setCycleDuration(Duration.millis(200)); - } - - @Override - protected void interpolate(final double frac) { - tabPaneContainer.setMaxHeight(((tabPaneContainer.getMaxHeight() - collapseHeight) * (1 - frac)) + collapseHeight); - } - }; - - initializeTabPane(); - initializeMessages(); - - collapseMessagesContainer.play(); - }); - } - - private void initializeTabPane() { - tabPane.getSelectionModel().selectedIndexProperty().addListener((obs, oldSelected, newSelected) -> { - if (newSelected.intValue() < 0 || isOpen.getValue()) return; - - if (shouldISkipOpeningTheMessagesContainer) { - tabPane.getSelectionModel().clearSelection(); - shouldISkipOpeningTheMessagesContainer = false; - } else { - expandMessagesIfNotExpanded(); - } - }); - - tabPane.getSelectionModel().clearSelection(); - isOpen.addListener((observable, oldValue, newValue) -> { - if (newValue) { - collapseMessagesIcon.setIconLiteral("gmi-close"); - } else { - collapseMessagesIcon.setIconLiteral("gmi-expand-less"); - } - - openCloseExternalAction.run(); - }); - - isOpen.setValue(false); - } - - private void initializeMessages() { - final Map componentMessageCollectionPresentationMapForErrors = new HashMap<>(); - final Map componentMessageCollectionPresentationMapForWarnings = new HashMap<>(); - - final Consumer addComponent = (component) -> { - final MessageCollectionPresentation messageCollectionPresentationErrors = new MessageCollectionPresentation(component, CodeAnalysis.getErrors(component)); - componentMessageCollectionPresentationMapForErrors.put(component, messageCollectionPresentationErrors); - errorsList.getChildren().add(messageCollectionPresentationErrors); - - final Runnable addIfErrors = () -> { - if (CodeAnalysis.getErrors(component).size() == 0) { - errorsList.getChildren().remove(messageCollectionPresentationErrors); - } else if (!errorsList.getChildren().contains(messageCollectionPresentationErrors)) { - errorsList.getChildren().add(messageCollectionPresentationErrors); - } - }; - - addIfErrors.run(); - CodeAnalysis.getErrors(component).addListener((ListChangeListener) c -> { - while (c.next()) { - addIfErrors.run(); - } - }); - - final MessageCollectionPresentation messageCollectionPresentationWarnings = new MessageCollectionPresentation(component, CodeAnalysis.getWarnings(component)); - componentMessageCollectionPresentationMapForWarnings.put(component, messageCollectionPresentationWarnings); - warningsList.getChildren().add(messageCollectionPresentationWarnings); - - final Runnable addIfWarnings = () -> { - if (CodeAnalysis.getWarnings(component).size() == 0) { - warningsList.getChildren().remove(messageCollectionPresentationWarnings); - } else if (!warningsList.getChildren().contains(messageCollectionPresentationWarnings)) { - warningsList.getChildren().add(messageCollectionPresentationWarnings); - } - }; - - addIfWarnings.run(); - CodeAnalysis.getWarnings(component).addListener((ListChangeListener) c -> { - while (c.next()) { - addIfWarnings.run(); - } - }); - }; - - // Add error that is project wide but not a backend error - addComponent.accept(null); - - Ecdar.getProject().getComponents().forEach(addComponent); - Ecdar.getProject().getComponents().addListener((ListChangeListener) c -> { - while (c.next()) { - c.getAddedSubList().forEach(addComponent::accept); - - c.getRemoved().forEach(component -> { - errorsList.getChildren().remove(componentMessageCollectionPresentationMapForErrors.get(component)); - componentMessageCollectionPresentationMapForErrors.remove(component); - - warningsList.getChildren().remove(componentMessageCollectionPresentationMapForWarnings.get(component)); - componentMessageCollectionPresentationMapForWarnings.remove(component); - }); - } - }); - - final Map messageMessagePresentationHashMap = new HashMap<>(); - - CodeAnalysis.getBackendErrors().addListener((ListChangeListener) c -> { - while (c.next()) { - c.getAddedSubList().forEach(addedMessage -> { - final MessagePresentation messagePresentation = new MessagePresentation(addedMessage); - backendErrorsList.getChildren().add(messagePresentation); - messageMessagePresentationHashMap.put(addedMessage, messagePresentation); - }); - - c.getRemoved().forEach(removedMessage -> { - backendErrorsList.getChildren().remove(messageMessagePresentationHashMap.get(removedMessage)); - messageMessagePresentationHashMap.remove(removedMessage); - }); - } - }); - } - - public void expandMessagesIfNotExpanded() { - if (!isOpen.getValue()) { - expandMessagesContainer.play(); - isOpen.setValue(true); - } - } - - public void collapseMessagesIfNotCollapsed() { - if (isOpen.getValue()) { - expandHeight = tabPaneContainer.getHeight(); - collapseMessagesContainer.play(); - isOpen.setValue(false); - } - } - - public void setRunnableForOpeningAndClosingMessageTabPane(Runnable runnable) { - openCloseExternalAction = runnable; - } - - public boolean isOpen() { - return isOpen.getValue(); - } - - /** - * Update the scale of root and all children - * - * @param scale the new scale of the tab pane - */ - public void updateScale(double scale) { - final double heightScaled = 35 * scale; - - collapseHeight = heightScaled; - tabPane.setTabMinHeight(heightScaled); - tabPane.setTabMaxHeight(heightScaled); - ((StackPane) collapseMessagesIcon.getParent()).setMinHeight(heightScaled); - ((StackPane) collapseMessagesIcon.getParent()).setMaxHeight(heightScaled); - ((StackPane) collapseMessagesIcon.getParent()).setMinWidth(heightScaled); - ((StackPane) collapseMessagesIcon.getParent()).setMaxWidth(heightScaled); - - if (!isOpen()) { - collapseMessagesContainer.play(); - } - } - - @FXML - public void collapseMessagesClicked() { - if (isOpen()) { - expandHeight = tabPaneContainer.getHeight(); - collapseMessagesContainer.play(); - } else { - expandMessagesContainer.play(); - } - - isOpen.setValue(!isOpen.getValue()); - } - - @FXML - public void tabPaneResizeElementPressed(final MouseEvent event) { - this.tabPanePreviousY = event.getScreenY(); - } - - @FXML - public void tabPaneResizeElementDragged(final MouseEvent event) { - expandMessagesIfNotExpanded(); - - final double mouseY = event.getScreenY(); - double newHeight = tabPaneContainer.getMaxHeight() - (mouseY - tabPanePreviousY); - newHeight = Math.max(collapseHeight, newHeight); - - tabPaneContainer.setMaxHeight(newHeight); - - openCloseExternalAction.run(); - this.tabPanePreviousY = mouseY; - } -} diff --git a/src/main/java/ecdar/controllers/ProcessController.java b/src/main/java/ecdar/controllers/ProcessController.java new file mode 100755 index 00000000..6ce42fc2 --- /dev/null +++ b/src/main/java/ecdar/controllers/ProcessController.java @@ -0,0 +1,252 @@ +package ecdar.controllers; + +import com.jfoenix.controls.JFXRippler; +import ecdar.abstractions.*; +import ecdar.presentations.SimEdgePresentation; +import ecdar.presentations.SimLocationPresentation; +import javafx.animation.Interpolator; +import javafx.animation.Transition; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableMap; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.*; +import javafx.scene.shape.Circle; +import javafx.util.Duration; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.math.BigDecimal; +import java.net.URL; +import java.util.*; + +/** + * The controller for the process shown in the {@link SimulatorOverviewController} + */ +public class ProcessController extends ModelController implements Initializable { + public StackPane componentPane; + public Pane modelContainerEdge; + public Pane modelContainerLocation; + public JFXRippler toggleValuesButton; + public VBox valueArea; + public FontIcon toggleValueButtonIcon; + private ObjectProperty component; + + /** + * Keep track of locations/edges and their associated presentation class, by having the as key-value pairs in a Map + * E.g. a {@link Location} key is the model behind the value {@link SimLocationPresentation} view + */ + private final Map locationPresentationMap = new HashMap<>(); + private final Map edgePresentationMap = new HashMap<>(); + private final ObservableMap variables = FXCollections.observableHashMap(); + private final ObservableMap clocks = FXCollections.observableHashMap(); + + @Override + public void initialize(final URL location, final ResourceBundle resources) { + component = new SimpleObjectProperty<>(new Component(true)); + initializeValues(); + } + + private void initializeValues() { + final Circle circle = new Circle(0); + if (getComponent().isDeclarationOpen()) { + circle.setRadius(1000); + } + final ObjectProperty clip = new SimpleObjectProperty<>(circle); + valueArea.clipProperty().bind(clip); + clip.set(circle); + } + + /** + * Highlights the edges and accompanying source/target locations in the process + * @param edges The edges to highlight + */ + public void highlightEdges(final ArrayList edges) { + for (final Edge edge : edges) { + final Location source = edge.getSourceLocation(); + String sourceId = source.getId(); + final Location target = edge.getTargetLocation(); + String targetId = target.getId(); + + // If target name is empty the edge is a self loop + if (Objects.equals(targetId, "")) { + targetId = sourceId; + } + + boolean isSourceUniversal = false; + + // Iterate through all locations to check for Universal and Inconsistent locations + // The name of a Universal location may be "U2" in our system, but it is mapped to "Universal" in the engine + // This loop maps "Universal" to for example "U2" + for (Map.Entry locEntry : locationPresentationMap.entrySet()) { + if (locEntry.getKey().getType() == Location.Type.UNIVERSAL) { + if (sourceId.equals("Universal")) { + sourceId = locEntry.getKey().getId(); + isSourceUniversal = true; + } + + if (targetId.equals("Universal")) { + targetId = locEntry.getKey().getId(); + } + } + + if (locEntry.getKey().getType() == Location.Type.INCONSISTENT) { + if (sourceId.equals("Inconsistent")) { + sourceId = locEntry.getKey().getId(); + } + + if (targetId.equals("Inconsistent")) { + targetId = locEntry.getKey().getId(); + } + } + } + + // Self loop on a Universal locations means that the edge name should be mapped to * + String edgeId = edge.getId(); + if (isSourceUniversal && sourceId.equals(targetId)) { + edgeId = "*"; + } + + highlightEdge(edgeId, edge.getStatus(), sourceId, targetId); + } + } + + /** + * Unhighlights all edges and locations in the process + */ + public void unhighlightProcess() { + edgePresentationMap.forEach((key, value) -> value.getController().unhighlight()); + locationPresentationMap.forEach((key, value) -> value.unhighlight()); + } + + /** + * Helper method that finds the {@link SimLocationPresentation} and highlights it. + * Calls {@link ProcessController#highlightEdgeLocations(String, String)} to highlight the source/targets locations + * @param edgeName The name of the edge + * @param edgeStatus The status (input/output) of the edge to highlight + * @param sourceName The name of the source location + * @param targetName The name of the target location + */ + private void highlightEdge(final String edgeName, final EdgeStatus edgeStatus, final String sourceName, final String targetName) { + for (Map.Entry entry: edgePresentationMap.entrySet()) { + final String keyName = entry.getKey().getSync(); + final String keySourceId = entry.getKey().getSourceLocation().getId(); + final String keyTargetId = entry.getKey().getTargetLocation().getId(); + + // Multiple edges may have the same name, so we also check that the source and target match this edge + if(keyName.equals(edgeName) && + keySourceId.equals(sourceName) && + keyTargetId.equals(targetName) && + entry.getKey().ioStatus.get() == edgeStatus) { + + entry.getValue().getController().highlight(); + highlightEdgeLocations(keySourceId, keyTargetId); + } + } + } + + /** + * Helper method that finds the source/target {@link SimLocationPresentation} and highlights it + * @param sourceId The name of the source location + * @param targetId The name of the target location + */ + private void highlightEdgeLocations(final String sourceId, final String targetId) { + for (Map.Entry locEntry: locationPresentationMap.entrySet()) { + final String locationId = locEntry.getKey().getId(); + + // Check if location is either source or target and highlight it + if(locationId.equals(sourceId) || locationId.equals(targetId)) { + locEntry.getValue().highlight(); + } + } + } + + /** + * Method that highlights all locations with the input ID + * @param locationId The locations to highlight + */ + public void highlightLocation(final String locationId) { + for (Map.Entry locEntry: locationPresentationMap.entrySet()) { + if(locEntry.getKey().getId().equals(locationId)) { + locEntry.getValue().highlight(); + } + } + } + + /** + * Sets the component which is going to be shown as a process.
+ * This also initializes the rest of the views needed for the process to be shown properly + * @param component the component of the process + */ + public void setComponent(final Component component){ + this.component.set(component); + modelContainerEdge.getChildren().clear(); + modelContainerLocation.getChildren().clear(); + + component.getLocations().forEach(location -> { + final SimLocationPresentation lp = new SimLocationPresentation(location, component); + modelContainerLocation.getChildren().add(lp); + locationPresentationMap.put(location, lp); + }); + + component.getEdges().forEach(edge -> { + final SimEdgePresentation ep = new SimEdgePresentation(edge, component); + modelContainerEdge.getChildren().add(ep); + edgePresentationMap.put(edge, ep); + }); + } + + public void toggleValues(final MouseEvent mouseEvent) { + final Circle circle = new Circle(0); + circle.setCenterX(component.get().getBox().getWidth() - (toggleValuesButton.getWidth() - mouseEvent.getX())); + circle.setCenterY(-1 * mouseEvent.getY()); + + final ObjectProperty clip = new SimpleObjectProperty<>(circle); + valueArea.clipProperty().bind(clip); + + final Transition rippleEffect = new Transition() { + private final double maxRadius = Math.sqrt(Math.pow(getComponent().getBox().getWidth(), 2) + Math.pow(getComponent().getBox().getHeight(), 2)); + { + setCycleDuration(Duration.millis(500)); + } + + protected void interpolate(final double fraction) { + if (getComponent().isDeclarationOpen()) { + circle.setRadius(fraction * maxRadius); + } else { + circle.setRadius(maxRadius - fraction * maxRadius); + } + clip.set(circle); + } + }; + + final Interpolator interpolator = Interpolator.SPLINE(0.785, 0.135, 0.15, 0.86); + rippleEffect.setInterpolator(interpolator); + + rippleEffect.play(); + getComponent().declarationOpenProperty().set(!getComponent().isDeclarationOpen()); + } + + /** + * Gets the component linked to this process + * @return the component of the process + */ + public Component getComponent(){ + return component.get(); + } + + @Override + public HighLevelModelObject getModel() { + return component.get(); + } + + public ObservableMap getVariables() { + return variables; + } + + public ObservableMap getClocks() { + return clocks; + } +} diff --git a/src/main/java/ecdar/controllers/ProjectPaneController.java b/src/main/java/ecdar/controllers/ProjectPaneController.java index c80bcbc1..8923cad2 100644 --- a/src/main/java/ecdar/controllers/ProjectPaneController.java +++ b/src/main/java/ecdar/controllers/ProjectPaneController.java @@ -12,6 +12,7 @@ import com.jfoenix.controls.JFXPopup; import com.jfoenix.controls.JFXRippler; import com.jfoenix.controls.JFXTextArea; +import javafx.application.Platform; import javafx.collections.ListChangeListener; import javafx.fxml.FXML; import javafx.fxml.Initializable; @@ -20,7 +21,6 @@ import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.image.ImageView; -import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; @@ -83,6 +83,8 @@ public void onChanged(final Change c) { c.getAddedSubList().forEach(this::handleAddedModel); c.getRemoved().forEach(this::handleRemovedModel); + generatedComponentsDivider.setVisible(!tempFilesList.getChildren().isEmpty()); + // Sort the children alphabetically sortPresentations(); } @@ -341,7 +343,7 @@ private void handleRemovedModel(final HighLevelModelObject model) { public void updateColorsOnFilePresentations() { for (Node child : filesList.getChildren()) { if (child instanceof FilePresentation) { - ((FilePresentation) child).updateColors(); + Platform.runLater(() -> ((FilePresentation) child).updateColors()); } } @@ -390,12 +392,12 @@ private void createSystemClicked() { */ @FXML private void setGeneratedComponentsVisibilityButtonClicked() { - if (generatedComponentsVisibilityButtonIcon.getIconCode() == Material.EXPAND_MORE) { - generatedComponentsVisibilityButtonIcon.setIconCode(Material.EXPAND_LESS); + if (generatedComponentsVisibilityButtonIcon.getIconCode() == Material.ARROW_LEFT) { + generatedComponentsVisibilityButtonIcon.setIconCode(Material.ARROW_DROP_DOWN); this.tempFilesList.setVisible(true); this.tempFilesList.setManaged(true); } else { - generatedComponentsVisibilityButtonIcon.setIconCode(Material.EXPAND_MORE); + generatedComponentsVisibilityButtonIcon.setIconCode(Material.ARROW_LEFT); this.tempFilesList.setVisible(false); this.tempFilesList.setManaged(false); } diff --git a/src/main/java/ecdar/controllers/QueryController.java b/src/main/java/ecdar/controllers/QueryController.java index a3f78280..6c584aa4 100644 --- a/src/main/java/ecdar/controllers/QueryController.java +++ b/src/main/java/ecdar/controllers/QueryController.java @@ -2,18 +2,22 @@ import com.jfoenix.controls.JFXComboBox; import com.jfoenix.controls.JFXRippler; +import com.jfoenix.controls.JFXTextField; import ecdar.abstractions.BackendInstance; import ecdar.abstractions.Query; import ecdar.abstractions.QueryType; import ecdar.backend.BackendHelper; import ecdar.utility.colors.Color; +import ecdar.utility.helpers.StringHelper; import javafx.application.Platform; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ObservableValue; +import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Tooltip; import javafx.scene.text.Text; import org.kordamp.ikonli.javafx.FontIcon; - +import javafx.beans.value.ChangeListener; import java.net.URL; import java.util.HashMap; import java.util.Map; @@ -27,10 +31,19 @@ public class QueryController implements Initializable { private Query query; private final Map queryTypeListElementsSelectedState = new HashMap<>(); private final Tooltip noQueryTypeSetTooltip = new Tooltip("Please select a query type beneath the status icon"); + @FXML + private JFXTextField queryText; @Override public void initialize(URL location, ResourceBundle resources) { initializeActionButton(); + + queryText.textProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, String oldValue, String newValue) { + queryText.setText(StringHelper.ConvertSymbolsToUnicode(newValue)); + } + }); } public void setQuery(Query query) { diff --git a/src/main/java/ecdar/controllers/QueryPaneController.java b/src/main/java/ecdar/controllers/QueryPaneController.java index 3e4a621d..ad2cef79 100644 --- a/src/main/java/ecdar/controllers/QueryPaneController.java +++ b/src/main/java/ecdar/controllers/QueryPaneController.java @@ -3,16 +3,9 @@ import ecdar.Ecdar; import ecdar.abstractions.Query; import ecdar.abstractions.QueryState; -import ecdar.backend.*; -import ecdar.presentations.Grid; import ecdar.presentations.QueryPresentation; import com.jfoenix.controls.JFXRippler; -import ecdar.utility.UndoRedoStack; import ecdar.utility.colors.Color; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleDoubleProperty; import javafx.collections.ListChangeListener; import javafx.fxml.FXML; import javafx.fxml.Initializable; @@ -21,9 +14,6 @@ import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.layout.*; -import javafx.scene.paint.Paint; -import javafx.scene.shape.Rectangle; - import java.net.URL; import java.util.HashMap; import java.util.Map; @@ -87,7 +77,7 @@ private void runAllQueriesButtonClicked() { Ecdar.getProject().getQueries().forEach(query -> { if (query.getType() == null) return; query.cancel(); - query.run(); + Ecdar.getQueryExecutor().executeQuery(query); }); } diff --git a/src/main/java/ecdar/controllers/RightSimPaneController.java b/src/main/java/ecdar/controllers/RightSimPaneController.java new file mode 100755 index 00000000..5b86fc1b --- /dev/null +++ b/src/main/java/ecdar/controllers/RightSimPaneController.java @@ -0,0 +1,20 @@ +package ecdar.controllers; + +import javafx.fxml.Initializable; +import javafx.scene.layout.*; + +import java.net.URL; +import java.util.ResourceBundle; + +/** + * Controller class for the right pane in the simulator + */ +public class RightSimPaneController implements Initializable { + public StackPane root; + public VBox scrollPaneVbox; +// public QueryPaneElementPresentation queryPaneElement; + + @Override + public void initialize(URL location, ResourceBundle resources) { + } +} diff --git a/src/main/java/ecdar/controllers/SimEdgeController.java b/src/main/java/ecdar/controllers/SimEdgeController.java new file mode 100755 index 00000000..2ca0048f --- /dev/null +++ b/src/main/java/ecdar/controllers/SimEdgeController.java @@ -0,0 +1,421 @@ +package ecdar.controllers; + +import ecdar.abstractions.Component; +import ecdar.abstractions.Edge; +import ecdar.abstractions.EdgeStatus; +import ecdar.abstractions.Nail; +import ecdar.model_canvas.arrow_heads.SimpleArrowHead; +import ecdar.presentations.Link; +import ecdar.presentations.NailPresentation; +import ecdar.presentations.SimNailPresentation; +import ecdar.utility.Highlightable; +import ecdar.utility.colors.Color; +import ecdar.utility.helpers.BindingHelper; +import ecdar.utility.helpers.Circular; +import ecdar.utility.helpers.ItemDragHelper; +import ecdar.utility.helpers.SelectHelper; +import ecdar.utility.keyboard.KeyboardTracker; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.application.Platform; +import javafx.beans.property.*; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.fxml.Initializable; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.util.Duration; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.function.Consumer; + +/** + * The controller for the edge shown in the {@link ecdar.presentations.SimulatorOverviewPresentation} + */ +public class SimEdgeController implements Initializable, Highlightable { + private final ObservableList links = FXCollections.observableArrayList(); + private final ObjectProperty edge = new SimpleObjectProperty<>(); + private final ObjectProperty component = new SimpleObjectProperty<>(); + private final SimpleArrowHead simpleArrowHead = new SimpleArrowHead(); + private final SimpleBooleanProperty isHoveringEdge = new SimpleBooleanProperty(false); + private final SimpleIntegerProperty timeHoveringEdge = new SimpleIntegerProperty(0); + private final Map nailNailPresentationMap = new HashMap<>(); + public Group edgeRoot; + private Runnable collapseNail; + private Consumer enlargeNail; + private Consumer shrinkNail; + + @Override + public void initialize(final URL location, final ResourceBundle resources) { + initializeNailCollapse(); + + edge.addListener((obsEdge, oldEdge, newEdge) -> { + newEdge.targetCircularProperty().addListener(getNewTargetCircularListener(newEdge)); + component.addListener(getComponentChangeListener(newEdge)); + + // Invalidate the list of edges (to update UI and errors) + newEdge.targetCircularProperty().addListener(observable -> { + getComponent().removeEdge(getEdge()); + getComponent().addEdge(getEdge()); + }); + + // When an edge updates highlight property, + // we want to update the view to reflect current highlight property + edge.get().isHighlightedProperty().addListener(v -> { + if(edge.get().getIsHighlighted()) { + this.highlight(); + } else { + this.unhighlight(); + } + }); + }); + + ensureNailsInFront(); + } + + private void ensureNailsInFront() { + // When ever changes happens to the children of the edge root force nails in front and other elements to back + edgeRoot.getChildren().addListener((ListChangeListener) c -> { + while (c.next()) { + for (int i = 0; i < c.getAddedSize(); i++) { + final Node node = c.getAddedSubList().get(i); + if (node instanceof NailPresentation) { + node.toFront(); + } else { + node.toBack(); + } + } + } + }); + } + + private ChangeListener getComponentChangeListener(final Edge newEdge) { + return (obsComponent, oldComponent, newComponent) -> { + // Draw new edge from a location + if (newEdge.getNails().isEmpty() && newEdge.getTargetCircular() == null) { + final Link link = new Link(); + // Make dashed line, if output edge + if (newEdge.getStatus() == EdgeStatus.OUTPUT) link.makeDashed(); + links.add(link); + + // Add the link and its arrowhead to the view + edgeRoot.getChildren().addAll(link, simpleArrowHead); + + // Bind the first link and the arrowhead from the source location to the mouse + BindingHelper.bind(link, simpleArrowHead, newEdge.getSourceCircular(), + newComponent.getBox().getXProperty(), newComponent.getBox().getYProperty()); + } else if (newEdge.getTargetCircular() != null) { + + edgeRoot.getChildren().add(simpleArrowHead); + + final Circular[] previous = {newEdge.getSourceCircular()}; + + newEdge.getNails().forEach(nail -> { + final Link link = new Link(); + if (newEdge.getStatus() == EdgeStatus.OUTPUT) link.makeDashed(); + links.add(link); + + final SimNailPresentation nailPresentation = new SimNailPresentation(nail, newEdge, getComponent(), this); + nailNailPresentationMap.put(nail, nailPresentation); + + edgeRoot.getChildren().addAll(link, nailPresentation); + BindingHelper.bind(link, previous[0], nail); + + previous[0] = nail; + }); + + final Link link = new Link(); + if (newEdge.getStatus() == EdgeStatus.OUTPUT) link.makeDashed(); + links.add(link); + + edgeRoot.getChildren().add(link); + BindingHelper.bind(link, simpleArrowHead, previous[0], newEdge.getTargetCircular()); + } + + // Changes are made to the nails list + newEdge.getNails().addListener(getNailsChangeListener(newEdge, newComponent)); + + }; + } + + private ListChangeListener getNailsChangeListener(final Edge newEdge, final Component newComponent) { + return change -> { + while (change.next()) { + // There were added some nails + change.getAddedSubList().forEach(newNail -> { + // Create a new nail presentation based on the abstraction added to the list + final SimNailPresentation newNailPresentation = new SimNailPresentation(newNail, newEdge, newComponent, this); + nailNailPresentationMap.put(newNail, newNailPresentation); + + edgeRoot.getChildren().addAll(newNailPresentation); + + if (newEdge.getTargetCircular() != null) { + final int indexOfNewNail = edge.get().getNails().indexOf(newNail); + + final Link newLink = new Link(); + if (newEdge.getStatus() == EdgeStatus.OUTPUT) newLink.makeDashed(); + final Link pressedLink = links.get(indexOfNewNail); + links.add(indexOfNewNail, newLink); + + edgeRoot.getChildren().addAll(newLink); + + Circular oldStart = getEdge().getSourceCircular(); + Circular oldEnd = getEdge().getTargetCircular(); + + if (indexOfNewNail != 0) { + oldStart = getEdge().getNails().get(indexOfNewNail - 1); + } + + if (indexOfNewNail != getEdge().getNails().size() - 1) { + oldEnd = getEdge().getNails().get(indexOfNewNail + 1); + } + + BindingHelper.bind(newLink, oldStart, newNail); + + if (oldEnd.equals(getEdge().getTargetCircular())) { + BindingHelper.bind(pressedLink, simpleArrowHead, newNail, oldEnd); + } else { + BindingHelper.bind(pressedLink, newNail, oldEnd); + } + + if (isHoveringEdge.get()) { + enlargeNail.accept(newNail); + } + + } else { + // The previous last link must end in the new nail + final Link lastLink = links.get(links.size() - 1); + + // If the nail is the first in the list, bind it to the source location + // otherwise, bind it the the previous nail + final int nailIndex = edge.get().getNails().indexOf(newNail); + if (nailIndex == 0) { + BindingHelper.bind(lastLink, newEdge.getSourceCircular(), newNail); + } else { + final Nail previousNail = edge.get().getNails().get(nailIndex - 1); + BindingHelper.bind(lastLink, previousNail, newNail); + } + + // Create a new link that will bind from the new nail to the mouse + final Link newLink = new Link(); + if (newEdge.getStatus() == EdgeStatus.OUTPUT) newLink.makeDashed(); + links.add(newLink); + BindingHelper.bind(newLink, simpleArrowHead, newNail, newComponent.getBox().getXProperty(), newComponent.getBox().getYProperty()); + edgeRoot.getChildren().add(newLink); + } + }); + + change.getRemoved().forEach(removedNail -> { + final int removedIndex = change.getFrom(); + final SimNailPresentation removedNailPresentation = nailNailPresentationMap.remove(removedNail); + final Link danglingLink = links.get(removedIndex + 1); + edgeRoot.getChildren().remove(removedNailPresentation); + edgeRoot.getChildren().remove(links.get(removedIndex)); + + Circular newFrom = getEdge().getSourceCircular(); + Circular newTo = getEdge().getTargetCircular(); + + if (removedIndex > 0) { + newFrom = getEdge().getNails().get(removedIndex - 1); + } + + if (removedIndex - 1 != getEdge().getNails().size() - 1) { + newTo = getEdge().getNails().get(removedIndex); + } + + if (newTo.equals(getEdge().getTargetCircular())) { + BindingHelper.bind(danglingLink, simpleArrowHead, newFrom, newTo); + } else { + BindingHelper.bind(danglingLink, newFrom, newTo); + } + links.remove(removedIndex); + }); + } + }; + } + + private ChangeListener getNewTargetCircularListener(final Edge newEdge) { + // When the target location is set, finish drawing the edge + return (obsTargetLocation, oldTargetCircular, newTargetCircular) -> { + // If the nails list is empty, directly connect the source and target locations + // otherwise, bind the line from the last nail to the target location + final Link lastLink = links.get(links.size() - 1); + final ObservableList nails = getEdge().getNails(); + if (nails.size() == 0) { + BindingHelper.bind(lastLink, simpleArrowHead, newEdge.getSourceCircular(), newEdge.getTargetCircular()); + } else { + final Nail lastNail = nails.get(nails.size() - 1); + BindingHelper.bind(lastLink, simpleArrowHead, lastNail, newEdge.getTargetCircular()); + } + + KeyboardTracker.unregisterKeybind(KeyboardTracker.ABANDON_EDGE); + + // When the target location is set the + edgeRoot.setMouseTransparent(false); + }; + } + + /** + * Initializes functionality to enlarge, shirk, and collapse nails + */ + private void initializeNailCollapse() { + enlargeNail = nail -> { + if (!nail.getPropertyType().equals(Edge.PropertyType.NONE)) return; + final Timeline animation = new Timeline(); + + final KeyValue radius0 = new KeyValue(nail.radiusProperty(), NailPresentation.COLLAPSED_RADIUS); + final KeyValue radius2 = new KeyValue(nail.radiusProperty(), NailPresentation.HOVERED_RADIUS * 1.2); + final KeyValue radius1 = new KeyValue(nail.radiusProperty(), NailPresentation.HOVERED_RADIUS); + + final KeyFrame kf1 = new KeyFrame(Duration.millis(0), radius0); + final KeyFrame kf2 = new KeyFrame(Duration.millis(80), radius2); + final KeyFrame kf3 = new KeyFrame(Duration.millis(100), radius1); + + animation.getKeyFrames().addAll(kf1, kf2, kf3); + animation.play(); + }; + shrinkNail = nail -> { + if (!nail.getPropertyType().equals(Edge.PropertyType.NONE)) return; + final Timeline animation = new Timeline(); + + final KeyValue radius0 = new KeyValue(nail.radiusProperty(), NailPresentation.COLLAPSED_RADIUS); + final KeyValue radius1 = new KeyValue(nail.radiusProperty(), NailPresentation.HOVERED_RADIUS); + + final KeyFrame kf1 = new KeyFrame(Duration.millis(0), radius1); + final KeyFrame kf2 = new KeyFrame(Duration.millis(100), radius0); + + animation.getKeyFrames().addAll(kf1, kf2); + animation.play(); + }; + + collapseNail = () -> { + final int interval = 50; + int previousValue = 1; + + try { + while (true) { + Thread.sleep(interval); + + if (isHoveringEdge.get()) { + // Do not let the timer go above this threshold + if (timeHoveringEdge.get() <= 500) { + timeHoveringEdge.set(timeHoveringEdge.get() + interval); + } + } else { + timeHoveringEdge.set(timeHoveringEdge.get() - interval); + } + + if (previousValue >= 0 && timeHoveringEdge.get() < 0) { + // Run on UI thread + Platform.runLater(() -> { + // Collapse all nails + getEdge().getNails().forEach(shrinkNail); + }); + break; + } + previousValue = timeHoveringEdge.get(); + } + + } catch (final InterruptedException e) { + e.printStackTrace(); + } + }; + } + + public Edge getEdge() { + return edge.get(); + } + + public void setEdge(final Edge edge) { + this.edge.set(edge); + } + + public ObjectProperty edgeProperty() { + return edge; + } + + public Component getComponent() { + return component.get(); + } + + public void setComponent(final Component component) { + this.component.set(component); + } + + public ObjectProperty componentProperty() { + return component; + } + + /** + * Colors the edge model + * @param color the new color of the edge + * @param intensity the intensity of the edge + */ + public void color(final Color color, final Color.Intensity intensity) { + final Edge edge = getEdge(); + + // Set the color of the edge + edge.setColorIntensity(intensity); + edge.setColor(color); + } + + public Color getColor() { + return getEdge().getColor(); + } + + public Color.Intensity getColorIntensity() { + return getEdge().getColorIntensity(); + } + + public ItemDragHelper.DragBounds getDragBounds() { + return ItemDragHelper.DragBounds.generateLooseDragBounds(); + } + + /*** + * Highlights the child nodes of the edge + */ + @Override + public void highlight() { + // Clear the currently selected elements, so we don't have multiple things highlighted/selected + SelectHelper.clearSelectedElements(); + edgeRoot.getChildren().forEach(node -> { + if(node instanceof Highlightable) { + ((Highlightable) node).highlight(); + } + }); + } + + /*** + * Removes the highlight from child nodes + */ + @Override + public void unhighlight() { + edgeRoot.getChildren().forEach(node -> { + if(node instanceof Highlightable) { + ((Highlightable) node).unhighlight(); + } + }); + } + + public DoubleProperty xProperty() { + return edgeRoot.layoutXProperty(); + } + + public DoubleProperty yProperty() { + return edgeRoot.layoutYProperty(); + } + + public double getX() { + return xProperty().get(); + } + + public double getY() { + return yProperty().get(); + } +} diff --git a/src/main/java/ecdar/controllers/SimLocationController.java b/src/main/java/ecdar/controllers/SimLocationController.java new file mode 100755 index 00000000..d5112fc4 --- /dev/null +++ b/src/main/java/ecdar/controllers/SimLocationController.java @@ -0,0 +1,172 @@ +package ecdar.controllers; + +import com.jfoenix.controls.JFXPopup; +import ecdar.Ecdar; +import ecdar.abstractions.*; +import ecdar.backend.BackendHelper; +import ecdar.presentations.DropDownMenu; +import ecdar.presentations.SimLocationPresentation; +import ecdar.presentations.SimTagPresentation; +import ecdar.utility.colors.Color; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.Initializable; +import javafx.scene.Group; +import javafx.scene.control.Label; +import javafx.scene.shape.Circle; +import javafx.scene.shape.Line; +import javafx.scene.shape.Path; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import java.util.function.Consumer; + +import java.net.URL; +import java.util.ResourceBundle; + +/** + * The controller of a location shown in the {@link ecdar.presentations.SimulatorOverviewPresentation} + */ +public class SimLocationController implements Initializable { + private final ObjectProperty location = new SimpleObjectProperty<>(); + private final ObjectProperty component = new SimpleObjectProperty<>(); + public SimLocationPresentation root; + public Path notCommittedShape; + public Path notCommittedInitialIndicator; + public Group shakeContent; + public Circle circle; + public Circle circleShakeIndicator; + public Group scaleContent; + public SimTagPresentation nicknameTag; + public SimTagPresentation invariantTag; + public Label idLabel; + public Line nameTagLine; + public Line invariantTagLine; + private DropDownMenu dropDownMenu; + + @Override + public void initialize(final URL location, final ResourceBundle resources) { + this.location.addListener((obsLocation, oldLocation, newLocation) -> { + // The radius property on the abstraction must reflect the radius in the view + newLocation.radiusProperty().bind(circle.radiusProperty()); + + // The scale property on the abstraction must reflect the radius in the view + newLocation.scaleProperty().bind(scaleContent.scaleXProperty()); + }); + + // Scale x and y 1:1 (based on the x-scale) + scaleContent.scaleYProperty().bind(scaleContent.scaleXProperty()); + initializeMouseControls(); + } + + private void initializeMouseControls() { + final Consumer mouseClicked = (event) -> { + if (root.isPlaced()) { + if (event.getButton().equals(MouseButton.SECONDARY)) { + initializeDropDownMenu(); + dropDownMenu.show(JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, 0, 0); + } + } + + }; + locationProperty().addListener((obs, oldLocation, newLocation) -> { + if(newLocation == null) return; + root.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClicked::accept); + }); + } + + /** + * Creates the dropdown when right clicking a location. + * When reachability is chosen a request will be send to the backend to see if the location can be reached + */ + public void initializeDropDownMenu(){ + dropDownMenu = new DropDownMenu(root); + + dropDownMenu.addClickableListElement("Is " + getLocation().getId() + " reachable?", event -> { + // Generate the query from the backend + final String reachabilityQuery = BackendHelper.getLocationReachableQuery(getLocation(), getComponent(), SimulatorController.getSimulationQuery()); + + // Add proper comment + final String reachabilityComment = "Is " + getLocation().getMostDescriptiveIdentifier() + " reachable?"; + + // Add new query for this location + final Query query = new Query(reachabilityQuery, reachabilityComment, QueryState.UNKNOWN); + query.setType(QueryType.REACHABILITY); + + // execute query + Ecdar.getQueryExecutor().executeQuery(query); + + dropDownMenu.hide(); + }); + } + public Location getLocation() { + return location.get(); + } + + /** + * Set/places the given location on the view. + * This have to be done before adding the {@link SimLocationPresentation} to the view as nothing + * would then be displayed. + * @param location the location + */ + public void setLocation(final Location location) { + this.location.set(location); + root.setLayoutX(location.getX()); + root.setLayoutY(location.getY()); + location.xProperty().bindBidirectional(root.layoutXProperty()); + location.yProperty().bindBidirectional(root.layoutYProperty()); + } + + public ObjectProperty locationProperty() { + return location; + } + + public Component getComponent() { + return component.get(); + } + + public void setComponent(final Component component) { + this.component.set(component); + } + + public ObjectProperty componentProperty() { + return component; + } + + /** + * Colors the location model + * @param color the new color of the location + * @param intensity the intensity of the color + */ + public void color(final Color color, final Color.Intensity intensity) { + final Location location = getLocation(); + + // Set the color of the location + location.setColorIntensity(intensity); + location.setColor(color); + } + + public Color getColor() { + return getLocation().getColor(); + } + + public Color.Intensity getColorIntensity() { + return getLocation().getColorIntensity(); + } + + public DoubleProperty xProperty() { + return root.layoutXProperty(); + } + + public DoubleProperty yProperty() { + return root.layoutYProperty(); + } + + public double getX() { + return xProperty().get(); + } + + public double getY() { + return yProperty().get(); + } +} diff --git a/src/main/java/ecdar/controllers/SimNailController.java b/src/main/java/ecdar/controllers/SimNailController.java new file mode 100755 index 00000000..053456d3 --- /dev/null +++ b/src/main/java/ecdar/controllers/SimNailController.java @@ -0,0 +1,126 @@ +package ecdar.controllers; + +import ecdar.Debug; +import ecdar.abstractions.Component; +import ecdar.abstractions.Edge; +import ecdar.abstractions.Nail; +import ecdar.presentations.SimNailPresentation; +import ecdar.presentations.SimTagPresentation; +import ecdar.utility.colors.Color; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.Initializable; +import javafx.scene.Group; +import javafx.scene.control.Label; +import javafx.scene.shape.Circle; +import javafx.scene.shape.Line; + +import java.net.URL; +import java.util.ResourceBundle; + +public class SimNailController implements Initializable { + private final ObjectProperty component = new SimpleObjectProperty<>(); + private final ObjectProperty edge = new SimpleObjectProperty<>(); + private final ObjectProperty nail = new SimpleObjectProperty<>(); + + private SimEdgeController edgeController; + public SimNailPresentation root; + public Circle nailCircle; + public Circle dragCircle; + public Line propertyTagLine; + public SimTagPresentation propertyTag; + public Group dragGroup; + public Label propertyLabel; + + @Override + public void initialize(final URL location, final ResourceBundle resources) { + nail.addListener((obsNail, oldNail, newNail) -> { + + // The radius from the abstraction is the master and the view simply reflects what is in the model + nailCircle.radiusProperty().bind(newNail.radiusProperty()); + + // Draw the presentation based on the initial value from the abstraction + root.setLayoutX(newNail.getX()); + root.setLayoutY(newNail.getY()); + + // Reflect future updates from the presentation into the abstraction + newNail.xProperty().bindBidirectional(root.layoutXProperty()); + newNail.yProperty().bindBidirectional(root.layoutYProperty()); + + }); + + // Debug visuals + dragCircle.opacityProperty().bind(Debug.draggableAreaOpacity); + dragCircle.setFill(Debug.draggableAreaColor.getColor(Debug.draggableAreaColorIntensity)); + } + + /** + * Sets an edge controller. + * This should be called when adding a nail. + * @param controller the edge controller + */ + public void setEdgeController(final SimEdgeController controller) { + this.edgeController = controller; + } + + public Nail getNail() { + return nail.get(); + } + + public void setNail(final Nail nail) { + this.nail.set(nail); + } + + public ObjectProperty nailProperty() { + return nail; + } + + public Component getComponent() { + return component.get(); + } + + public void setComponent(final Component component) { + this.component.set(component); + } + + public ObjectProperty componentProperty() { + return component; + } + + public Edge getEdge() { + return edge.get(); + } + + public void setEdge(final Edge edge) { + this.edge.set(edge); + } + + public ObjectProperty edgeProperty() { + return edge; + } + + public Color getColor() { + return getComponent().getColor(); + } + + public Color.Intensity getColorIntensity() { + return getComponent().getColorIntensity(); + } + + public DoubleProperty xProperty() { + return root.layoutXProperty(); + } + + public DoubleProperty yProperty() { + return root.layoutYProperty(); + } + + public double getX() { + return xProperty().get(); + } + + public double getY() { + return yProperty().get(); + } +} diff --git a/src/main/java/ecdar/controllers/SimulationInitializationDialogController.java b/src/main/java/ecdar/controllers/SimulationInitializationDialogController.java new file mode 100644 index 00000000..a34787a6 --- /dev/null +++ b/src/main/java/ecdar/controllers/SimulationInitializationDialogController.java @@ -0,0 +1,44 @@ +package ecdar.controllers; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXComboBox; +import javafx.fxml.Initializable; + +import java.net.URL; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SimulationInitializationDialogController implements Initializable { + public JFXComboBox simulationComboBox; + public JFXButton cancelButton; + public JFXButton startButton; + + public static List ListOfComponents = new ArrayList<>(); + /** + * Function gets list of components to simulation + * and saves it in the public static ListOfComponents + */ + public void setSimulationData(){ + // set simulation query + SimulatorController.setSimulationQuery(simulationComboBox.getSelectionModel().getSelectedItem()); + + // set list of components involved in simulation + ListOfComponents.clear(); + // pattern filters out all components by ignoring operators. + Pattern pattern = Pattern.compile("([\\w]*)", Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(SimulatorController.getSimulationQuery()); + List listOfComponentsToSimulate = new ArrayList<>(); + //Adds all found components to list. + while(matcher.find()){ + if(matcher.group().length() != 0) { + listOfComponentsToSimulate.add(matcher.group()); + } + } + ListOfComponents = listOfComponentsToSimulate; + } + + public void initialize(URL location, ResourceBundle resources) { + + } +} diff --git a/src/main/java/ecdar/controllers/SimulatorController.java b/src/main/java/ecdar/controllers/SimulatorController.java new file mode 100644 index 00000000..cacdc467 --- /dev/null +++ b/src/main/java/ecdar/controllers/SimulatorController.java @@ -0,0 +1,144 @@ +package ecdar.controllers; + +import ecdar.Ecdar; +import ecdar.abstractions.*; +import ecdar.backend.SimulationHandler; +import ecdar.presentations.SimulatorOverviewPresentation; +import ecdar.simulation.SimulationState; +import ecdar.utility.colors.Color; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.Initializable; +import javafx.scene.layout.StackPane; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; + +public class SimulatorController implements Initializable { + private static String simulationQuery; + public StackPane root; + public SimulatorOverviewPresentation overviewPresentation; + public StackPane toolbar; + + private boolean firstTimeInSimulator; + private final static DoubleProperty width = new SimpleDoubleProperty(), + height = new SimpleDoubleProperty(); + private static ObjectProperty selectedState = new SimpleObjectProperty<>(); + + @Override + public void initialize(URL location, ResourceBundle resources) { + root.widthProperty().addListener((observable, oldValue, newValue) -> width.setValue(newValue)); + root.heightProperty().addListener((observable, oldValue, newValue) -> height.setValue(newValue)); + firstTimeInSimulator = true; + } + + /** + * Prepares the simulator to be shown.
+ * It also prepares the processes to be shown in the {@link SimulatorOverviewPresentation} by:
+ * - Building the system if it has been updated or have never been created.
+ * - Adding the components which are going to be used in the simulation to + */ + public void willShow() { + final SimulationHandler sm = Ecdar.getSimulationHandler(); + boolean shouldSimulationBeReset = true; + + + + // If the user left a trace, continue from that trace + if (sm.traceLog.size() >= 2) { + shouldSimulationBeReset = false; + } + + // If the composition is not the same as previous simulation, reset the simulation + if (!(overviewPresentation.getController().getComponentObservableList().hashCode() == + findComponentsInCurrentSimulation(SimulationInitializationDialogController.ListOfComponents).hashCode())) { + shouldSimulationBeReset = true; + } + + if (shouldSimulationBeReset || firstTimeInSimulator || sm.currentState.get() == null) { + resetSimulation(); + sm.initialStep(); + } + + overviewPresentation.getController().addProcessesToGroup(); + + // If the simulation continues, highligt the current state and available edges + if (sm.currentState.get() != null && !shouldSimulationBeReset) { + overviewPresentation.getController().highlightProcessState(sm.currentState.get()); + overviewPresentation.getController().highlightAvailableEdges(sm.currentState.get()); + } + + } + + /** + * Resets the current simulation, and prepares for a new simulation by clearing the + * {@link SimulatorOverviewController#processContainer} and adding the processes of the new simulation. + */ + private void resetSimulation() { + List listOfComponentsForSimulation = findComponentsInCurrentSimulation(SimulationInitializationDialogController.ListOfComponents); + overviewPresentation.getController().clearOverview(); + overviewPresentation.getController().getComponentObservableList().clear(); + overviewPresentation.getController().getComponentObservableList().addAll(listOfComponentsForSimulation); + firstTimeInSimulator = false; + } + + /** + * Finds the components that are used in the current simulation by looking at the components found in + * Ecdar.getProject.getComponents() and compares them to the components found in the queryComponents list + * + * @return all the components used in the current simulation + */ + private List findComponentsInCurrentSimulation(List queryComponents) { + //Show components from the system + List components = Ecdar.getProject().getComponents(); + + //Matches query components against with existing components and adds them to simulation + List SelectedComponents = new ArrayList<>(); + for(Component comp : components) { + for(String componentInQuery : queryComponents) { + if((comp.getName().equals(componentInQuery))) { + Component temp = new Component(comp.serialize()); + SelectedComponents.add(temp); + } + } + } + return SelectedComponents; + } + + /** + * Resets the simulation and prepares the view for showing the new simulation to the user + */ + public void resetCurrentSimulation() { + overviewPresentation.getController().removeProcessesFromGroup(); + resetSimulation(); + Ecdar.getSimulationHandler().resetToInitialLocation(); + overviewPresentation.getController().addProcessesToGroup(); + } + + public void willHide() { + overviewPresentation.getController().removeProcessesFromGroup(); + } + + public static DoubleProperty getWidthProperty() { + return width; + } + + public static DoubleProperty getHeightProperty() { + return height; + } + + public static void setSelectedState(SimulationState selectedState) { + SimulatorController.selectedState.set(selectedState); + } + public static void setSimulationQuery(String query) { + simulationQuery = query; + } + + public static String getSimulationQuery(){ + return simulationQuery; + } +} diff --git a/src/main/java/ecdar/controllers/SimulatorOverviewController.java b/src/main/java/ecdar/controllers/SimulatorOverviewController.java new file mode 100644 index 00000000..b4815785 --- /dev/null +++ b/src/main/java/ecdar/controllers/SimulatorOverviewController.java @@ -0,0 +1,393 @@ +package ecdar.controllers; + +import ecdar.Ecdar; +import ecdar.abstractions.*; +import ecdar.presentations.ProcessPresentation; +import ecdar.simulation.SimulationState; +import ecdar.simulation.Transition; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.beans.InvalidationListener; +import javafx.collections.*; +import javafx.fxml.Initializable; +import javafx.geometry.Insets; +import javafx.scene.Group; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.*; +import javafx.util.Pair; + +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * The controller of the middle part of the simulator. + * It is here where processes of a simulation will be shown. + */ +public class SimulatorOverviewController implements Initializable { + public AnchorPane root; + public ScrollPane scrollPane; + public FlowPane processContainer; + public Group groupContainer; + + /** + * The amount that is going be zoomed in/out for each press on + or - + */ + private final double SCALE_DELTA = 1.1; + + /** + * The max that the user can zoom in + */ + private static final double MAX_ZOOM_IN = 1.6; + + /** + * The max that the user can zoom in + */ + private static final double MAX_ZOOM_OUT = 0.5; + + /** + * Offset such that the view does not overlap with the scroll bar on the right hand sig. + */ + private static final int SUPER_SPECIAL_SCROLLPANE_OFFSET = 20; + + private final ObservableList componentArrayList = FXCollections.observableArrayList(); + private final ObservableMap processPresentations = FXCollections.observableHashMap(); + + /** + * Is true if a reset of the zoom have been requested, false if not. + */ + private boolean resetZoom = false; + private boolean isMaxZoomInReached = false; + private boolean isMaxZoomOutReached = false; + + @Override + public void initialize(final URL location, final ResourceBundle resources) { + groupContainer = new Group(); + processContainer = new FlowPane(); + //In case that the processContainer gets moved around we have to keep in into place. + initializeProcessContainer(); + + initializeWindowResizing(); + initializeZoom(); + initializeSimulationVariables(); + initializeHighlighting(); + // Add the processes and group to the view + addProcessesToGroup(); + scrollPane.setContent(groupContainer); + } + + /** + * Initializes the {@link #processContainer} with its correct styling, and placement on the view. + * It also adds a {@link ListChangeListener} on {@link #componentArrayList} where it adds the + * {@link Component}s which are needed to the processContainer. + */ + private void initializeProcessContainer() { + processContainer.translateXProperty().addListener((observable, oldValue, newValue) -> { + processContainer.setTranslateX(0); + }); + //Sets the space between the processes + processContainer.setHgap(10); + processContainer.setVgap(10); + + // padding to the scrollpane + processContainer.setPadding(new Insets(5)); + + componentArrayList.addListener((ListChangeListener) c -> { + final Map processes = new HashMap<>(); + while (c.next()) { + if (c.wasRemoved()) { + clearOverview(); + } else { + c.getAddedSubList().forEach(o -> processes.put(o.getName(), new ProcessPresentation(o))); + } + } + // Highlight the current state when the processes change + highlightProcessState(Ecdar.getSimulationHandler().currentState.get()); // ToDo NIELS: Throws NullPointerException inside method due to currentState + processContainer.getChildren().addAll(processes.values()); + processPresentations.putAll(processes); + }); + + final Map processes = new HashMap<>(); + componentArrayList.forEach(o -> processes.put(o.getName(), new ProcessPresentation(o))); + + processContainer.getChildren().addAll(processes.values()); + processPresentations.putAll(processes); + } + + /** + * Clears the {@link #processContainer} and the {@link #processPresentations}. + */ + void clearOverview() { + processContainer.getChildren().clear(); + processPresentations.clear(); + } + + /** + * Setup listeners for displaying clock and variable values on the {@link ProcessPresentation} + */ + private void initializeSimulationVariables() { + Ecdar.getSimulationHandler().getSimulationVariables().addListener((InvalidationListener) obs -> { + Ecdar.getSimulationHandler().getSimulationVariables().forEach((s, bigDecimal) -> { + if (!s.equals("t(0)")) {// As t(0) does not belong to any process + final String[] spittedString = s.split("\\."); + // If the process containing the var is not there we just skip it + if (spittedString.length > 0 && processPresentations.size() > 0) { + processPresentations.get(spittedString[0]).getController().getVariables().put(spittedString[1], bigDecimal); + } + } + }); + }); + Ecdar.getSimulationHandler().getSimulationClocks().addListener((InvalidationListener) obs -> { + if (processPresentations.size() == 0) return; + Ecdar.getSimulationHandler().getSimulationClocks().forEach((s, bigDecimal) -> { + if (!s.equals("t(0)")) {// As t(0) does not belong to any process + final String[] spittedString = s.split("\\."); + // If the process containing the clock is not there we just skip it + if (spittedString.length > 0 && processPresentations.size() > 0) { + processPresentations.get(spittedString[0]).getController().getClocks().put(spittedString[1], bigDecimal); + } + } + }); + }); + } + + /** + * Removes {@link #processContainer} from the {@link #groupContainer}.
+ * In this way the {@link Component}s in the processContainer will then again be resizable, + * as the class {@link Group} makes its children not resizeable. + * + * @see Group + */ + void removeProcessesFromGroup() { + groupContainer.getChildren().removeAll(processContainer); + } + + /** + * Adds the {@link #processContainer} to the {@link #groupContainer}.
+ * This method is usually needed to called if {@link #removeProcessesFromGroup()} have been called, or + * if the processContainer just need to be added to the groupContainer.
+ * This method makes sure that the processContainer will be added to the groupContainer + * which is needed to show the {@link ProcessPresentation}s in the {@link #scrollPane}. + * If the processContainer is already contained in the groupContainer + * the method does nothing but return. + * + * @see #removeProcessesFromGroup() + */ + void addProcessesToGroup() { + if (groupContainer.getChildren().contains(processContainer)) return; + groupContainer.getChildren().add(processContainer); + } + + /** + * Initializes the zoom functionality in {@link #processContainer} + */ + private void initializeZoom() { + processContainer.scaleXProperty().addListener((observable, oldValue, newValue) -> { + if (newValue.doubleValue() > MAX_ZOOM_IN) isMaxZoomInReached = true; + if (newValue.doubleValue() < MAX_ZOOM_OUT) isMaxZoomOutReached = true; + + handleWidthOnScale(oldValue, newValue); + }); + + // to support pinch zooming + //TODO this should be fixed at as it does not work as it should + /* + processContainer.setOnZoom(event -> { + //Tries to zoom in/out but max is reached + if(event.getZoomFactor() >= 1 && isMaxZoomInReached) return; + if(event.getZoomFactor() < 1 && isMaxZoomOutReached) return; + + isMaxZoomInReached = false; + isMaxZoomOutReached = false; + + processContainer.setScaleX(processContainer.getScaleX() * event.getZoomFactor()); + processContainer.setScaleY(processContainer.getScaleY() * event.getZoomFactor()); + });*/ + } + + /** + * Initializes listener for change of width in {@link #scrollPane} which also affects {@link #processContainer}
+ * This does also take the zooming into account when doing the resizing. + */ + private void initializeWindowResizing() { + scrollPane.widthProperty().addListener((observable, oldValue, newValue) -> { + final double width = (newValue.doubleValue()) * (1 + (1 - processContainer.getScaleX())); + if (processContainer.getScaleX() > 1) { //Zoomed in + processContainer.setMinWidth(width); + processContainer.setMaxWidth(width); + } else if (processContainer.getScaleX() < 1) { //Zoomed out + processContainer.setMinWidth(width); + processContainer.setMaxWidth(width); + final double deltaWidth = newValue.doubleValue() - groupContainer.layoutBoundsProperty().get().getWidth(); + processContainer.setMinWidth(processContainer.getWidth() + (deltaWidth - SUPER_SPECIAL_SCROLLPANE_OFFSET) * (1 + (1 - processContainer.getScaleX()))); + processContainer.setMaxWidth(processContainer.getWidth() + (deltaWidth - SUPER_SPECIAL_SCROLLPANE_OFFSET) * (1 + (1 - processContainer.getScaleX()))); + } else { // Reset + processContainer.setMinWidth(newValue.doubleValue() - SUPER_SPECIAL_SCROLLPANE_OFFSET); + processContainer.setMaxWidth(newValue.doubleValue() - SUPER_SPECIAL_SCROLLPANE_OFFSET); + } + }); + } + + /** + * Increments the {@link #processContainer} scaleX and scaleY properties + * which creates the zoom-in feeling. Resizing of the view is handled by {@link #handleWidthOnScale(Number, Number)} + * + * @see FlowPane#scaleXProperty() + * @see FlowPane#scaleYProperty() + */ + void zoomIn() { + if (isMaxZoomInReached) return; + isMaxZoomOutReached = false; + processContainer.setScaleX(processContainer.getScaleX() * SCALE_DELTA); + processContainer.setScaleY(processContainer.getScaleY() * SCALE_DELTA); + } + + + /** + * Decrements the {@link #processContainer} scaleX and scaleY properties + * which creates the zoom-in feeling. Resizing of the view is handled by {@link #handleWidthOnScale(Number, Number)} + * + * @see FlowPane#scaleXProperty() + * @see FlowPane#scaleYProperty() + */ + void zoomOut() { + if (isMaxZoomOutReached) return; + isMaxZoomInReached = false; + processContainer.setScaleX(processContainer.getScaleX() * (1 / SCALE_DELTA)); + processContainer.setScaleY(processContainer.getScaleY() * (1 / SCALE_DELTA)); + } + + /** + * Resets the scaling of the {@link #processContainer}, and hereby the zoom + * + * @see FlowPane#scaleXProperty() + * @see FlowPane#scaleYProperty() + */ + void resetZoom() { + if (processContainer.getScaleX() == 1) return; + resetZoom = true; + isMaxZoomInReached = false; + isMaxZoomOutReached = false; + processContainer.setScaleX(1); + processContainer.setScaleY(1); + } + + /** + * Handles the scaling of the width of the {@link #processContainer} + * + * @param oldValue the width of {@link #scrollPane} before the change + * @param newValue the width of {@link #scrollPane} after the change + */ + private void handleWidthOnScale(final Number oldValue, final Number newValue) { + if (resetZoom) { //Zoom reset + resetZoom = false; + processContainer.setMinWidth(scrollPane.getWidth() - SUPER_SPECIAL_SCROLLPANE_OFFSET); + processContainer.setMaxWidth(scrollPane.getWidth() - SUPER_SPECIAL_SCROLLPANE_OFFSET); + } else if (oldValue.doubleValue() > newValue.doubleValue()) { //Zoom in + resetZoom = false; + processContainer.setMinWidth(Math.round(processContainer.getWidth() * SCALE_DELTA)); + processContainer.setMaxWidth(Math.round(processContainer.getWidth() * SCALE_DELTA)); + } else { // Zoom out + resetZoom = false; + processContainer.setMinWidth(Math.round(processContainer.getWidth() * (1 / SCALE_DELTA))); + processContainer.setMaxWidth(Math.round(processContainer.getWidth() * (1 / SCALE_DELTA))); + } + } + + /** + * Initializer method to setup listeners that handle highlighting when selected/current state/transition changes + */ + private void initializeHighlighting() { + Ecdar.getSimulationHandler().selectedEdge.addListener((observable, oldEdge, newEdge) -> { + unhighlightProcesses(); + }); + + Ecdar.getSimulationHandler().currentState.addListener((observable, oldState, newState) -> { + if (newState == null) { + return; + } + unhighlightProcesses(); + highlightProcessState(newState); + highlightAvailableEdges(newState); + }); + } + + /** + * Highlights all the processes involved in the transition. + * Finds the processes involved in the transition (processes with edges in the transition) and highlights their edges + * Also fades processes that are not active in the selected transition + * + * @param transition The transition for which we highlight the involved processes + */ + public void highlightProcessTransition(final Transition transition) { + final var edges = transition.getEdges(); + + // List of all processes to show as inactive if they are not involved in a transition + // Processes are removed from this list, if they have an edge in the transition + final ArrayList processesToHide = new ArrayList<>(processPresentations.values()); + + for (final ProcessPresentation processPresentation : processPresentations.values()) { + + // Find the processes that have edges involved in this transition + processPresentation.getController().highlightEdges(edges); + processesToHide.remove(processPresentation); + } + + processesToHide.forEach(ProcessPresentation::showInactive); + } + + + /** + * Unhighlights all processes + */ + public void unhighlightProcesses() { + for (final ProcessPresentation presentation : processPresentations.values()) { + presentation.getController().unhighlightProcess(); + presentation.showActive(); + } + } + + /** + * Finds the processes for the input locations in the input {@link SimulationState} and highlights the locations. + * + * @param state The state with the locations to highlight + */ + public void highlightProcessState(final SimulationState state) { + if (state == null) return; + for (int i = 0; i < state.getLocations().size(); i++) { + final Pair loc = state.getLocations().get(i); + + processPresentations.values().stream() + .filter(p -> p.getController().getComponent().getName().equals(loc.getKey())) + .forEach(p -> p.getController().highlightLocation(loc.getValue())); + } + } + + public ObservableList getComponentObservableList() { + return componentArrayList; + } + + public void highlightAvailableEdges(SimulationState state) { + // unhighlight all edges + for (Pair edge : state.getEdges()) { + processPresentations.values().stream() + .forEach(p -> p.getController().getComponent().getEdges().stream() + .forEach(e -> e.setIsHighlighted(false))); + } + + // highlight available edges in the given state + for (Pair edge : state.getEdges()) { + processPresentations.values().stream() + .forEach(p -> p.getController().getComponent().getEdges().stream() + .forEach(e -> { + if (e.getId().equals(edge.getValue())) { + e.setIsHighlighted(true); + } + })); + } + } + +} diff --git a/src/main/java/ecdar/controllers/SystemEdgeController.java b/src/main/java/ecdar/controllers/SystemEdgeController.java index f8e46cf5..de846446 100644 --- a/src/main/java/ecdar/controllers/SystemEdgeController.java +++ b/src/main/java/ecdar/controllers/SystemEdgeController.java @@ -16,7 +16,6 @@ import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; -import javafx.scene.layout.Pane; import javafx.scene.shape.Circle; import java.net.URL; diff --git a/src/main/java/ecdar/controllers/TracePaneElementController.java b/src/main/java/ecdar/controllers/TracePaneElementController.java new file mode 100755 index 00000000..01c27bf0 --- /dev/null +++ b/src/main/java/ecdar/controllers/TracePaneElementController.java @@ -0,0 +1,194 @@ +package ecdar.controllers; + +import com.jfoenix.controls.JFXRippler; +import ecdar.Ecdar; +import ecdar.abstractions.Location; +import ecdar.backend.SimulationHandler; +import ecdar.simulation.SimulationState; +import ecdar.presentations.TransitionPresentation; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.collections.ListChangeListener; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Label; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.net.URL; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * The controller class for the trace pane element that can be inserted into a simulator pane + */ +public class TracePaneElementController implements Initializable { + public VBox root; + public HBox toolbar; + public Label traceTitle; + public JFXRippler expandTrace; + public VBox traceList; + public FontIcon expandTraceIcon; + public AnchorPane traceSummary; + public Label summaryTitleLabel; + public Label summarySubtitleLabel; + + private SimpleBooleanProperty isTraceExpanded = new SimpleBooleanProperty(false); + private Map transitionPresentationMap = new LinkedHashMap<>(); + private SimpleIntegerProperty numberOfSteps = new SimpleIntegerProperty(0); + + @Override + public void initialize(URL location, ResourceBundle resources) { + Ecdar.getSimulationHandler().getTraceLog().addListener((ListChangeListener) c -> { + while (c.next()) { + for (final SimulationState state : c.getAddedSubList()) { + if (state != null) insertTraceState(state, true); + } + + for (final SimulationState state : c.getRemoved()) { + traceList.getChildren().remove(transitionPresentationMap.get(state)); + transitionPresentationMap.remove(state); + } + } + + numberOfSteps.set(transitionPresentationMap.size()); + }); + + initializeTraceExpand(); + } + + /** + * Initializes the expand functionality that allows the user to show or hide the trace. + * By default the trace is shown. + */ + private void initializeTraceExpand() { + isTraceExpanded.addListener((obs, oldVal, newVal) -> { + if (newVal) { + showTrace(); + expandTraceIcon.setIconLiteral("gmi-expand-less"); + expandTraceIcon.setIconSize(24); + } else { + hideTrace(); + expandTraceIcon.setIconLiteral("gmi-expand-more"); + expandTraceIcon.setIconSize(24); + } + }); + + isTraceExpanded.set(true); + } + + + /** + * Removes all the trace view elements as to hide the trace from the user + * Also shows the summary view when the trace is hidden + */ + private void hideTrace() { + traceList.getChildren().clear(); + root.getChildren().add(traceSummary); + } + + /** + * Shows the trace by inserting a {@link TransitionPresentation} for each trace state + * Also hides the summary view, since it should only be visible when the trace is hidden + */ + private void showTrace() { + transitionPresentationMap.forEach((state, presentation) -> { + insertTraceState(state, false); + }); + root.getChildren().remove(traceSummary); + } + + /** + * Instantiates a {@link TransitionPresentation} for a {@link SimulationState} and adds it to the view + * + * @param state The state the should be inserted into the trace log + * @param shouldAnimate A boolean that indicates whether the trace should fade in when added to the view + */ + private void insertTraceState(final SimulationState state, final boolean shouldAnimate) { + final TransitionPresentation transitionPresentation = new TransitionPresentation(); + transitionPresentationMap.put(state, transitionPresentation); + + transitionPresentation.setOnMouseReleased(event -> { + event.consume(); + final SimulationHandler simHandler = Ecdar.getSimulationHandler(); + if (simHandler == null) return; + Ecdar.getSimulationHandler().selectStateFromLog(state); + }); + + EventHandler mouseEntered = transitionPresentation.getOnMouseEntered(); + transitionPresentation.setOnMouseEntered(event -> { + SimulatorController.setSelectedState(state); + mouseEntered.handle(event); + }); + + EventHandler mouseExited = transitionPresentation.getOnMouseExited(); + transitionPresentation.setOnMouseExited(event -> { + SimulatorController.setSelectedState(null); + mouseExited.handle(event); + }); + + String title = traceString(state); + transitionPresentation.getController().setTitle(title); + + // Only insert the presentation into the view if the trace is expanded & state is not null + if (isTraceExpanded.get() && state != null) { + traceList.getChildren().add(transitionPresentation); + if (shouldAnimate) { + transitionPresentation.playFadeAnimation(); + } + } + } + + /** + * A helper method that returns a string representing a state in the trace log + * + * @param state The SimulationState to represent + * @return A string representing the state + */ + private String traceString(SimulationState state) { + StringBuilder title = new StringBuilder("("); + int length = state.getLocations().size(); + for (int i = 0; i < length; i++) { + Location loc = Ecdar.getProject() + .findComponent(state.getLocations().get(i).getKey()) + .findLocation(state.getLocations().get(i).getValue()); + String locationName = loc.getId(); + if (i == length - 1) { + title.append(locationName); + } else { + title.append(locationName).append(", "); + } + } + title.append(")\n"); + + StringBuilder clocks = new StringBuilder(); + for (var constraint : state.getState().getFederation().getDisjunction().getConjunctions(0).getConstraintsList()) { + var x = constraint.getX().getClockName(); + var y = constraint.getY().getClockName(); + var c = constraint.getC(); + var strict = constraint.getStrict(); + clocks.append(x).append(" - ").append(y).append(strict ? " < " : " <= ").append(c).append("\n"); + } + return title.toString() + clocks.toString(); + } + + /** + * Method to be called when clicking on the expand rippler in the trace toolbar + */ + @FXML + private void expandTrace() { + if (isTraceExpanded.get()) { + isTraceExpanded.set(false); + } else { + isTraceExpanded.set(true); + } + } + + public SimpleIntegerProperty getNumberOfStepsProperty() { + return numberOfSteps; + } +} diff --git a/src/main/java/ecdar/controllers/TransitionController.java b/src/main/java/ecdar/controllers/TransitionController.java new file mode 100755 index 00000000..7bacda2e --- /dev/null +++ b/src/main/java/ecdar/controllers/TransitionController.java @@ -0,0 +1,45 @@ +package ecdar.controllers; + +import com.jfoenix.controls.JFXRippler; +import ecdar.simulation.Transition; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.Initializable; +import javafx.scene.control.Label; +import javafx.scene.layout.AnchorPane; + +import java.net.URL; +import java.util.ResourceBundle; + +/** + * The controller class for the transition view element. + * It represents a single transition and may be used by classes like {@see TransitionPaneElementController} + * to show a list of transitions + */ +public class TransitionController implements Initializable { + public AnchorPane root; + public Label titleLabel; + public JFXRippler rippler; + + // The transition that the view represents + private SimpleObjectProperty transition = new SimpleObjectProperty<>(); + private SimpleObjectProperty title = new SimpleObjectProperty<>(); + + @Override + public void initialize(URL location, ResourceBundle resources) { + title.addListener(((observable, oldValue, newValue) -> { + titleLabel.setText(newValue); + })); + } + + public void setTitle(String title) { + this.title.set(title); + } + + public void setTransition(Transition transition) { + this.transition.set(transition); + } + + public Transition getTransition() { + return transition.get(); + } +} diff --git a/src/main/java/ecdar/controllers/TransitionPaneElementController.java b/src/main/java/ecdar/controllers/TransitionPaneElementController.java new file mode 100755 index 00000000..30b355ff --- /dev/null +++ b/src/main/java/ecdar/controllers/TransitionPaneElementController.java @@ -0,0 +1,215 @@ +package ecdar.controllers; + +import com.jfoenix.controls.JFXRippler; +import com.jfoenix.controls.JFXTextField; +import ecdar.Ecdar; +import ecdar.abstractions.Edge; +import ecdar.simulation.Transition; +import ecdar.presentations.TransitionPresentation; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.math.BigDecimal; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * The controller class for the transition pane element that can be inserted into the simulator panes + */ +public class TransitionPaneElementController implements Initializable { + public VBox root; + public VBox transitionList; + public HBox toolbar; + public Label toolbarTitle; + public JFXRippler refreshRippler; + public JFXRippler expandTransition; + public FontIcon expandTransitionIcon; + public VBox delayChooser; + public JFXTextField delayTextField; + + private SimpleBooleanProperty isTransitionExpanded = new SimpleBooleanProperty(false); + private Map transitionPresentationMap = new HashMap<>(); + private SimpleObjectProperty delay = new SimpleObjectProperty<>(BigDecimal.ZERO); + + @Override + public void initialize(URL location, ResourceBundle resources) { + initializeTransitionExpand(); + initializeDelayChooser(); + } + + /** + * Sets up listeners for the delay chooser. + * Listens for changes in text property and updates the textfield with a sanitized value (e.g. no letters in delay). + * Also listens for changes in focus, so there's always a value in the textfield, even if the user deleted the text. + * Adds tooltip for the textfield. + */ + private void initializeDelayChooser() { + delayTextField.textProperty().addListener(((observable, oldValue, newValue) -> { + delayTextChanged(oldValue, newValue); + })); + + delayTextField.focusedProperty().addListener((observable, oldValue, newValue) -> { + // If the textfield loses focus and the user didn't enter anything + // show the value 0.0 + if(!newValue && delay.get().equals(BigDecimal.ZERO)) { + delayTextField.setText("0.0"); + } + }); + + Tooltip.install(delayTextField, new Tooltip("Enter delay to use for next transition")); + } + + /** + * Initializes the expand functionality that allows the user to show or hide the transitions. + * By default the transitions are shown. + */ + private void initializeTransitionExpand() { + isTransitionExpanded.addListener((obs, oldVal, newVal) -> { + if(newVal) { + if(!root.getChildren().contains(delayChooser)) { + // Add the delay chooser just below the toolbar + root.getChildren().add(1, delayChooser); + } + showTransitions(); + expandTransitionIcon.setIconLiteral("gmi-expand-less"); + expandTransitionIcon.setIconSize(24); + } else { + root.getChildren().remove(delayChooser); + hideTransitions(); + expandTransitionIcon.setIconLiteral("gmi-expand-more"); + expandTransitionIcon.setIconSize(24); + } + }); + + isTransitionExpanded.set(true); + } + + /** + * Removes all the transition view elements as to hide the transitions from the user + */ + private void hideTransitions() { + transitionList.getChildren().clear(); + } + + /** + * Shows the available transitions by inserting a {@link TransitionPresentation} for each transition + */ + private void showTransitions() { + transitionPresentationMap.forEach((transition, presentation) -> { + insertTransition(transition); + }); + } + + /** + * Instantiates a TransitionPresentation for a Transition and adds it to the view + * @param transition The transition that should be inserted into the view + */ + private void insertTransition(Transition transition) { + final TransitionPresentation transitionPresentation = new TransitionPresentation(); + String title = transitionString(transition); + transitionPresentation.getController().setTitle(title); + transitionPresentation.getController().setTransition(transition); + + // Update the selected transition when mouse entered. + // Add the event to existing mouseEntered events + // e.g. TransitionPresentation already has mouseEntered functionality and we want to keep it + EventHandler mouseEntered = transitionPresentation.getOnMouseEntered(); + // transitionPresentation.setOnMouseEntered(event -> { + // SimulatorController.setSelectedTransition(transitionPresentation.getController().getTransition()); + // mouseEntered.handle(event); + // }); + + EventHandler mouseExited = transitionPresentation.getOnMouseExited(); + transitionPresentation.setOnMouseExited(event -> { + mouseExited.handle(event); + }); + + transitionPresentationMap.put(transition, transitionPresentation); + + // Only insert the presentation into the view if the transitions are expanded + // Avoids inserting duplicate elements in the view (it's still added to the map) + if(isTransitionExpanded.get()) { + transitionList.getChildren().add(transitionPresentation); + } + } + + /** + * A helper method that returns a string representing a transition in the transition chooser + * @param transition The {@link Transition} to represent + * @return A string representing the transition + */ + private String transitionString(Transition transition) { + String title = transition.getLabel(); + if(transition.getEdges() != null) { + for (Edge edge : transition.getEdges()) { + title += " " + edge.getId(); + } + } + return title; + } + + /** + * Method to be called when clicking on the expand rippler in the transition toolbar + */ + @FXML + private void expandTransitions() { + if(isTransitionExpanded.get()) { + isTransitionExpanded.set(false); + } else { + isTransitionExpanded.set(true); + } + } + + /** + * Restart simulation to the initial step. + */ + @FXML + private void restartSimulation() { + Ecdar.getSimulationHandler().resetToInitialLocation(); + } + + /** + * Sanitizes the input that the user inserts into the delay textfield. + * Checks if the text can be converted into a BigDecimal otherwise show the previous value. + * For example avoids users entering letters. + * @param oldValue The old value to show if the newvalue cannot be converted to a BigDecimal + * @param newValue The new value to show in the textfield + */ + @FXML + private void delayTextChanged(String oldValue, String newValue) { + // If the value is empty (the user deleted the value), assume that the value is 0.0 but do not update the text + if(newValue.isEmpty()) { + this.delay.set(BigDecimal.ZERO); + } else { + // Try to convert the new value into a BigDecimal + // Note that we don't setText here, as the new value is already shown in the textfield + try { + BigDecimal bd = new BigDecimal(newValue); + + // Checking the string for "-" instead of whether bd is negative is due to the case of -0.0 + // So checking the string is just simpler + if(newValue.contains("-")) { + throw new NumberFormatException(); + } + + this.delay.set(bd); + + } catch (NumberFormatException ex) { + // If the conversion was not possible, show the old value + this.delayTextField.setText(oldValue); + } + } + + } + +} diff --git a/src/main/java/ecdar/mutation/ExportHandler.java b/src/main/java/ecdar/mutation/ExportHandler.java index fb5455cb..d75dcdfc 100644 --- a/src/main/java/ecdar/mutation/ExportHandler.java +++ b/src/main/java/ecdar/mutation/ExportHandler.java @@ -3,9 +3,6 @@ import com.google.gson.GsonBuilder; import ecdar.Ecdar; import ecdar.abstractions.Component; -import ecdar.abstractions.Project; -import ecdar.backend.BackendException; -import ecdar.backend.BackendHelper; import ecdar.mutation.models.MutationTestCase; import ecdar.mutation.models.MutationTestPlan; import ecdar.mutation.operators.MutationOperator; diff --git a/src/main/java/ecdar/mutation/TestCaseGenerationHandler.java b/src/main/java/ecdar/mutation/TestCaseGenerationHandler.java index 7d610874..8f408cbd 100644 --- a/src/main/java/ecdar/mutation/TestCaseGenerationHandler.java +++ b/src/main/java/ecdar/mutation/TestCaseGenerationHandler.java @@ -2,12 +2,10 @@ import ecdar.Ecdar; import ecdar.abstractions.Component; -import ecdar.abstractions.Project; import ecdar.backend.BackendException; import ecdar.backend.BackendHelper; import ecdar.mutation.models.MutationTestCase; import ecdar.mutation.models.MutationTestPlan; -import ecdar.mutation.models.NonRefinementStrategy; import javafx.application.Platform; import javafx.scene.paint.Color; import javafx.scene.text.Text; diff --git a/src/main/java/ecdar/presentations/DropDownMenu.java b/src/main/java/ecdar/presentations/DropDownMenu.java index a631b7ff..308f0033 100644 --- a/src/main/java/ecdar/presentations/DropDownMenu.java +++ b/src/main/java/ecdar/presentations/DropDownMenu.java @@ -14,7 +14,6 @@ import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; import javafx.scene.input.MouseEvent; import javafx.scene.layout.*; import javafx.scene.shape.Circle; @@ -24,7 +23,7 @@ import org.kordamp.ikonli.javafx.FontIcon; import javax.swing.*; -import java.util.Stack; + import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -90,6 +89,7 @@ public DropDownMenu(final Node src) { * @param width The width of the {@link DropDownMenu} */ public DropDownMenu(final Node src, final int width) { + dropDownMenuWidth.set(width); list = new VBox(); list.setStyle("-fx-background-color: white; -fx-padding: 8 0 8 0;"); diff --git a/src/main/java/ecdar/presentations/EcdarPresentation.java b/src/main/java/ecdar/presentations/EcdarPresentation.java index f8fb5f25..0648b67d 100644 --- a/src/main/java/ecdar/presentations/EcdarPresentation.java +++ b/src/main/java/ecdar/presentations/EcdarPresentation.java @@ -7,10 +7,6 @@ import ecdar.controllers.EcdarController; import ecdar.utility.UndoRedoStack; import ecdar.utility.colors.Color; -import ecdar.utility.colors.EnabledColor; -import ecdar.utility.helpers.SelectHelper; -import com.jfoenix.controls.JFXPopup; -import com.jfoenix.controls.JFXRippler; import com.jfoenix.controls.JFXSnackbar; import ecdar.utility.keyboard.Keybind; import ecdar.utility.keyboard.KeyboardTracker; @@ -22,89 +18,65 @@ import javafx.beans.property.SimpleDoubleProperty; import javafx.collections.ListChangeListener; import javafx.geometry.Insets; -import javafx.scene.control.Label; -import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.layout.*; -import javafx.scene.shape.Circle; import javafx.util.Duration; -import javafx.util.Pair; - -import java.util.ArrayList; -import java.util.List; - -import static ecdar.utility.colors.EnabledColor.enabledColors; public class EcdarPresentation extends StackPane { private final EcdarController controller; - private final BooleanProperty filePaneOpen = new SimpleBooleanProperty(false); - private final SimpleDoubleProperty filePaneAnimationProperty = new SimpleDoubleProperty(0); - private final BooleanProperty queryPaneOpen = new SimpleBooleanProperty(false); - private final SimpleDoubleProperty queryPaneAnimationProperty = new SimpleDoubleProperty(0); - private Timeline openQueryPaneAnimation; - private Timeline closeQueryPaneAnimation; - private Timeline openFilePaneAnimation; - private Timeline closeFilePaneAnimation; + private final BooleanProperty leftPaneOpen = new SimpleBooleanProperty(false); + private final SimpleDoubleProperty leftPaneAnimationProperty = new SimpleDoubleProperty(0); + private final BooleanProperty rightPaneOpen = new SimpleBooleanProperty(false); + private final SimpleDoubleProperty rightPaneAnimationProperty = new SimpleDoubleProperty(0); + private Timeline openLeftPaneAnimation; + private Timeline closeLeftPaneAnimation; + private Timeline openRightPaneAnimation; + private Timeline closeRightPaneAnimation; public EcdarPresentation() { controller = new EcdarFXMLLoader().loadAndGetController("EcdarPresentation.fxml", this); initializeTopBar(); - initializeToolbar(); initializeQueryDetailsDialog(); - initializeColorSelector(); - - initializeToggleQueryPaneFunctionality(); - initializeToggleFilePaneFunctionality(); - - initializeSelectDependentToolbarButton(controller.colorSelected); - Tooltip.install(controller.colorSelected, new Tooltip("Colour")); - - initializeSelectDependentToolbarButton(controller.deleteSelected); - Tooltip.install(controller.deleteSelected, new Tooltip("Delete")); - - initializeToolbarButton(controller.undo); - initializeToolbarButton(controller.redo); - initializeUndoRedoButtons(); + initializeToggleLeftPaneFunctionality(); + initializeToggleRightPaneFunctionality(); initializeSnackbar(); - // Open the file and query panel initially + // Open the left and right panes initially Platform.runLater(() -> { // Bind sizing of sides and center panes to ensure correct sizing - controller.canvasPane.minWidthProperty().bind(controller.root.widthProperty().subtract(filePaneAnimationProperty.add(queryPaneAnimationProperty))); - controller.canvasPane.maxWidthProperty().bind(controller.root.widthProperty().subtract(filePaneAnimationProperty.add(queryPaneAnimationProperty))); + controller.getEditorPresentation().getController().canvasPane.minWidthProperty().bind(controller.root.widthProperty().subtract(leftPaneAnimationProperty.add(rightPaneAnimationProperty))); + controller.getEditorPresentation().getController().canvasPane.maxWidthProperty().bind(controller.root.widthProperty().subtract(leftPaneAnimationProperty.add(rightPaneAnimationProperty))); // Bind the height to ensure that both the top and bottom panes are shown // The height of the top pane is multiplied by 4 as the UI does not account for the height otherwise - controller.canvasPane.minHeightProperty().bind(controller.root.heightProperty().subtract(controller.topPane.heightProperty().multiply(4).add(controller.bottomFillerElement.heightProperty()))); - controller.canvasPane.maxHeightProperty().bind(controller.root.heightProperty().subtract(controller.topPane.heightProperty().multiply(4).add(controller.bottomFillerElement.heightProperty()))); + controller.getEditorPresentation().getController().canvasPane.minHeightProperty().bind(controller.root.heightProperty().subtract(controller.topPane.heightProperty().multiply(4).add(controller.bottomFillerElement.heightProperty()))); + controller.getEditorPresentation().getController().canvasPane.maxHeightProperty().bind(controller.root.heightProperty().subtract(controller.topPane.heightProperty().multiply(4).add(controller.bottomFillerElement.heightProperty()))); - controller.leftPane.minWidthProperty().bind(filePaneAnimationProperty); - controller.leftPane.maxWidthProperty().bind(filePaneAnimationProperty); + controller.leftPane.minWidthProperty().bind(leftPaneAnimationProperty); + controller.leftPane.maxWidthProperty().bind(leftPaneAnimationProperty); - controller.rightPane.minWidthProperty().bind(queryPaneAnimationProperty); - controller.rightPane.maxWidthProperty().bind(queryPaneAnimationProperty); + controller.rightPane.minWidthProperty().bind(rightPaneAnimationProperty); + controller.rightPane.maxWidthProperty().bind(rightPaneAnimationProperty); controller.topPane.minHeightProperty().bind(controller.menuBar.heightProperty()); controller.topPane.maxHeightProperty().bind(controller.menuBar.heightProperty()); - Platform.runLater(() -> { - toggleFilePane(); - toggleQueryPane(); - }); + toggleLeftPane(); + toggleRightPane(); Ecdar.getPresentation().controller.scalingProperty.addListener((observable, oldValue, newValue) -> { // If the scaling has changed trigger animations for open panes to update width Platform.runLater(() -> { - if (filePaneOpen.get()) { - openFilePaneAnimation.play(); + if (leftPaneOpen.get()) { + openLeftPaneAnimation.play(); } - if (queryPaneOpen.get()) { - openQueryPaneAnimation.play(); + if (rightPaneOpen.get()) { + openRightPaneAnimation.play(); } }); @@ -130,147 +102,6 @@ private void initializeSnackbar() { controller.snackbar.autosize(); } - private void initializeUndoRedoButtons() { - UndoRedoStack.canUndoProperty().addListener((obs, oldState, newState) -> { - if (newState) { - // Enable the undo button - controller.undo.setEnabled(true); - controller.undo.setOpacity(1); - } else { - // Disable the undo button - controller.undo.setEnabled(false); - controller.undo.setOpacity(0.3); - } - }); - - UndoRedoStack.canRedoProperty().addListener((obs, oldState, newState) -> { - if (newState) { - // Enable the redo button - controller.redo.setEnabled(true); - controller.redo.setOpacity(1); - } else { - // Disable the redo button - controller.redo.setEnabled(false); - controller.redo.setOpacity(0.3); - } - }); - - // Disable the undo button - controller.undo.setEnabled(false); - controller.undo.setOpacity(0.3); - - // Disable the redo button - controller.redo.setEnabled(false); - controller.redo.setOpacity(0.3); - - // Set tooltips - Tooltip.install(controller.undo, new Tooltip("Undo")); - Tooltip.install(controller.redo, new Tooltip("Redo")); - } - - private void initializeColorSelector() { - final JFXPopup popup = new JFXPopup(); - - final double listWidth = 136; - final FlowPane list = new FlowPane(); - for (final EnabledColor color : enabledColors) { - final Circle circle = new Circle(16, color.color.getColor(color.intensity)); - circle.setStroke(color.color.getColor(color.intensity.next(2))); - circle.setStrokeWidth(1); - - final Label label = new Label(color.keyCode.getName()); - label.getStyleClass().add("subhead"); - label.setTextFill(color.color.getTextColor(color.intensity)); - - final StackPane child = new StackPane(circle, label); - child.setMinSize(40, 40); - child.setMaxSize(40, 40); - - child.setOnMouseEntered(event -> { - final ScaleTransition scaleTransition = new ScaleTransition(Duration.millis(100), circle); - scaleTransition.setFromX(circle.getScaleX()); - scaleTransition.setFromY(circle.getScaleY()); - scaleTransition.setToX(1.1); - scaleTransition.setToY(1.1); - scaleTransition.play(); - }); - - child.setOnMouseExited(event -> { - final ScaleTransition scaleTransition = new ScaleTransition(Duration.millis(100), circle); - scaleTransition.setFromX(circle.getScaleX()); - scaleTransition.setFromY(circle.getScaleY()); - scaleTransition.setToX(1.0); - scaleTransition.setToY(1.0); - scaleTransition.play(); - }); - - child.setOnMouseClicked(event -> { - final List> previousColor = new ArrayList<>(); - - SelectHelper.getSelectedElements().forEach(selectable -> { - previousColor.add(new Pair<>(selectable, new EnabledColor(selectable.getColor(), selectable.getColorIntensity()))); - }); - - controller.changeColorOnSelectedElements(color, previousColor); - - popup.hide(); - SelectHelper.clearSelectedElements(); - }); - - list.getChildren().add(child); - } - list.setMinWidth(listWidth); - list.setMaxWidth(listWidth); - list.setStyle("-fx-background-color: white; -fx-padding: 8;"); - - popup.setPopupContent(list); - - controller.colorSelected.setOnMouseClicked((e) -> { - // If nothing is selected - if (SelectHelper.getSelectedElements().size() == 0) return; - popup.show(controller.colorSelected, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -10, 15); - }); - } - - private void initializeSelectDependentToolbarButton(final JFXRippler button) { - initializeToolbarButton(button); - - // The color button should only be enabled when an element is selected - SelectHelper.getSelectedElements().addListener(new ListChangeListener() { - @Override - public void onChanged(final Change c) { - if (SelectHelper.getSelectedElements().size() > 0) { - button.setEnabled(true); - - final FadeTransition fadeAnimation = new FadeTransition(Duration.millis(100), button); - fadeAnimation.setFromValue(button.getOpacity()); - fadeAnimation.setToValue(1); - fadeAnimation.play(); - } else { - button.setEnabled(false); - - final FadeTransition fadeAnimation = new FadeTransition(Duration.millis(100), button); - fadeAnimation.setFromValue(1); - fadeAnimation.setToValue(0.3); - fadeAnimation.play(); - } - } - }); - - // Disable the button - button.setEnabled(false); - button.setOpacity(0.3); - } - - private void initializeToolbarButton(final JFXRippler button) { - final Color color = Color.GREY_BLUE; - final Color.Intensity colorIntensity = Color.Intensity.I800; - - button.setMaskType(JFXRippler.RipplerMask.CIRCLE); - button.setRipplerFill(color.getTextColor(colorIntensity)); - button.setPosition(JFXRippler.RipplerPos.BACK); - } - private void initializeQueryDetailsDialog() { final Color modalBarColor = Color.GREY_BLUE; final Color.Intensity modalBarColorIntensity = Color.Intensity.I500; @@ -283,106 +114,114 @@ private void initializeQueryDetailsDialog() { ))); } - private void initializeToggleFilePaneFunctionality() { - initializeOpenFilePaneAnimation(); - initializeCloseFilePaneAnimation(); + private void initializeToggleLeftPaneFunctionality() { + initializeOpenLeftPaneAnimation(); + initializeCloseLeftPaneAnimation(); // Translate the x coordinate to create the open/close animations - controller.filePane.translateXProperty().bind(filePaneAnimationProperty.subtract(controller.filePane.widthProperty())); + controller.projectPane.translateXProperty().bind(leftPaneAnimationProperty.subtract(controller.projectPane.widthProperty())); + controller.leftSimPane.translateXProperty().bind(leftPaneAnimationProperty.subtract(controller.leftSimPane.widthProperty())); // Whenever the width of the file pane is updated, update the animations - controller.filePane.widthProperty().addListener((observable) -> { - initializeOpenFilePaneAnimation(); - initializeCloseFilePaneAnimation(); + controller.projectPane.widthProperty().addListener((observable) -> { + initializeOpenLeftPaneAnimation(); + initializeCloseLeftPaneAnimation(); + }); + + // Whenever the width of the file pane is updated, update the animations + controller.leftPane.widthProperty().addListener((observable) -> { + initializeOpenLeftPaneAnimation(); + initializeCloseLeftPaneAnimation(); }); } - private void initializeCloseFilePaneAnimation() { + private void initializeCloseLeftPaneAnimation() { final Interpolator interpolator = Interpolator.SPLINE(0.645, 0.045, 0.355, 1); - closeFilePaneAnimation = new Timeline(); + closeLeftPaneAnimation = new Timeline(); - final KeyValue open = new KeyValue(filePaneAnimationProperty, controller.filePane.getWidth(), interpolator); - final KeyValue closed = new KeyValue(filePaneAnimationProperty, 0, interpolator); + final KeyValue open = new KeyValue(leftPaneAnimationProperty, controller.projectPane.getWidth(), interpolator); + final KeyValue closed = new KeyValue(leftPaneAnimationProperty, 0, interpolator); final KeyFrame kf1 = new KeyFrame(Duration.millis(0), open); final KeyFrame kf2 = new KeyFrame(Duration.millis(200), closed); - closeFilePaneAnimation.getKeyFrames().addAll(kf1, kf2); + closeLeftPaneAnimation.getKeyFrames().addAll(kf1, kf2); } - private void initializeOpenFilePaneAnimation() { + private void initializeOpenLeftPaneAnimation() { final Interpolator interpolator = Interpolator.SPLINE(0.645, 0.045, 0.355, 1); - openFilePaneAnimation = new Timeline(); + openLeftPaneAnimation = new Timeline(); - final KeyValue closed = new KeyValue(filePaneAnimationProperty, 0, interpolator); - final KeyValue open = new KeyValue(filePaneAnimationProperty, controller.filePane.getWidth(), interpolator); + final KeyValue closed = new KeyValue(leftPaneAnimationProperty, 0, interpolator); + final KeyValue open = new KeyValue(leftPaneAnimationProperty, controller.projectPane.getWidth(), interpolator); final KeyFrame kf1 = new KeyFrame(Duration.millis(0), closed); final KeyFrame kf2 = new KeyFrame(Duration.millis(200), open); - openFilePaneAnimation.getKeyFrames().addAll(kf1, kf2); + openLeftPaneAnimation.getKeyFrames().addAll(kf1, kf2); } - private void initializeToggleQueryPaneFunctionality() { - initializeOpenQueryPaneAnimation(); - initializeCloseQueryPaneAnimation(); + private void initializeToggleRightPaneFunctionality() { + initializeOpenRightPaneAnimation(); + initializeCloseRightPaneAnimation(); // Translate the x coordinate to create the open/close animations - controller.queryPane.translateXProperty().bind(queryPaneAnimationProperty.multiply(-1).add(controller.queryPane.widthProperty())); + controller.queryPane.translateXProperty().bind(rightPaneAnimationProperty.multiply(-1).add(controller.queryPane.widthProperty())); + controller.rightSimPane.translateXProperty().bind(rightPaneAnimationProperty.multiply(-1).add(controller.rightSimPane.widthProperty())); // Whenever the width of the query pane is updated, update the animations controller.queryPane.widthProperty().addListener((observable) -> { - initializeOpenQueryPaneAnimation(); - initializeCloseQueryPaneAnimation(); + initializeOpenRightPaneAnimation(); + initializeCloseRightPaneAnimation(); }); // When new queries are added, make sure that the query pane is open Ecdar.getProject().getQueries().addListener((ListChangeListener) c -> { - if (closeQueryPaneAnimation == null) + if (closeRightPaneAnimation == null) return; // The query pane is not yet initialized while (c.next()) { c.getAddedSubList().forEach(o -> { - if (!queryPaneOpen.get()) { + if (!rightPaneOpen.get()) { // Open the pane - openQueryPaneAnimation.play(); + openRightPaneAnimation.play(); // Toggle the open state - queryPaneOpen.set(queryPaneOpen.not().get()); + rightPaneOpen.set(rightPaneOpen.not().get()); } }); } }); } - private void initializeCloseQueryPaneAnimation() { + private void initializeCloseRightPaneAnimation() { final Interpolator interpolator = Interpolator.SPLINE(0.645, 0.045, 0.355, 1); - closeQueryPaneAnimation = new Timeline(); + closeRightPaneAnimation = new Timeline(); - final KeyValue open = new KeyValue(queryPaneAnimationProperty, controller.queryPane.getWidth(), interpolator); - final KeyValue closed = new KeyValue(queryPaneAnimationProperty, 0, interpolator); + final KeyValue open = new KeyValue(rightPaneAnimationProperty, controller.queryPane.getWidth(), interpolator); + final KeyValue closed = new KeyValue(rightPaneAnimationProperty, 0, interpolator); final KeyFrame kf1 = new KeyFrame(Duration.millis(0), open); final KeyFrame kf2 = new KeyFrame(Duration.millis(200), closed); - closeQueryPaneAnimation.getKeyFrames().addAll(kf1, kf2); + closeRightPaneAnimation.getKeyFrames().addAll(kf1, kf2); } - private void initializeOpenQueryPaneAnimation() { + private void initializeOpenRightPaneAnimation() { final Interpolator interpolator = Interpolator.SPLINE(0.645, 0.045, 0.355, 1); - openQueryPaneAnimation = new Timeline(); + openRightPaneAnimation = new Timeline(); - final KeyValue closed = new KeyValue(queryPaneAnimationProperty, 0, interpolator); - final KeyValue open = new KeyValue(queryPaneAnimationProperty, controller.queryPane.getWidth(), interpolator); + final KeyValue closed = new KeyValue(rightPaneAnimationProperty, 0, interpolator); + final KeyValue open = new KeyValue(rightPaneAnimationProperty, controller.queryPane.getWidth(), interpolator); final KeyFrame kf1 = new KeyFrame(Duration.millis(0), closed); final KeyFrame kf2 = new KeyFrame(Duration.millis(200), open); - openQueryPaneAnimation.getKeyFrames().addAll(kf1, kf2); + openRightPaneAnimation.getKeyFrames().addAll(kf1, kf2); } private void initializeTopBar() { @@ -405,18 +244,6 @@ private void initializeTopBar() { ))); } - private void initializeToolbar() { - final Color color = Color.GREY_BLUE; - final Color.Intensity intensity = Color.Intensity.I700; - - // Set the background for the top toolbar - controller.toolbar.setBackground( - new Background(new BackgroundFill(color.getColor(intensity), - CornerRadii.EMPTY, - Insets.EMPTY) - )); - } - /** * Initialize help image views. */ @@ -451,37 +278,36 @@ private void initializeResizeQueryPane() { // Set bounds for resizing to be between 280px and half the screen width final double newWidth = Math.min(Math.max(prevWidth.get() + diff, 280), controller.root.getWidth() / 2); - queryPaneAnimationProperty.set(newWidth); + rightPaneAnimationProperty.set(newWidth); controller.queryPane.setMaxWidth(newWidth); controller.queryPane.setMinWidth(newWidth); }); } - - public BooleanProperty toggleFilePane() { - if (filePaneOpen.get()) { - closeFilePaneAnimation.play(); + public BooleanProperty toggleLeftPane() { + if (leftPaneOpen.get()) { + closeLeftPaneAnimation.play(); } else { - openFilePaneAnimation.play(); + openLeftPaneAnimation.play(); } // Toggle the open state - filePaneOpen.set(filePaneOpen.not().get()); + leftPaneOpen.set(leftPaneOpen.not().get()); - return filePaneOpen; + return leftPaneOpen; } - public BooleanProperty toggleQueryPane() { - if (queryPaneOpen.get()) { - closeQueryPaneAnimation.play(); + public BooleanProperty toggleRightPane() { + if (rightPaneOpen.get()) { + closeRightPaneAnimation.play(); } else { - openQueryPaneAnimation.play(); + openRightPaneAnimation.play(); } // Toggle the open state - queryPaneOpen.set(queryPaneOpen.not().get()); + rightPaneOpen.set(rightPaneOpen.not().get()); - return queryPaneOpen; + return rightPaneOpen; } public static void fitSizeWhenAvailable(final ImageView imageView, final StackPane pane) { @@ -506,8 +332,8 @@ public void showSnackbarMessage(final String message) { } public void showHelp() { - controller.dialogContainer.setVisible(true); - controller.dialog.show(controller.dialogContainer); + controller.modellingHelpDialogContainer.setVisible(true); + controller.modellingHelpDialog.show(controller.modellingHelpDialogContainer); } public EcdarController getController() { diff --git a/src/main/java/ecdar/presentations/EdgePresentation.java b/src/main/java/ecdar/presentations/EdgePresentation.java index 993743bd..262374ec 100644 --- a/src/main/java/ecdar/presentations/EdgePresentation.java +++ b/src/main/java/ecdar/presentations/EdgePresentation.java @@ -2,6 +2,7 @@ import ecdar.abstractions.Component; import ecdar.abstractions.DisplayableEdge; +import ecdar.abstractions.Nail; import ecdar.controllers.EdgeController; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -15,12 +16,24 @@ public class EdgePresentation extends Group { public EdgePresentation(final DisplayableEdge edge, final Component component) { controller = new EcdarFXMLLoader().loadAndGetController("EdgePresentation.fxml", this); - controller.setEdge(edge); this.edge.bind(controller.edgeProperty()); controller.setComponent(component); this.component.bind(controller.componentProperty()); + initializeFailingEdgeListener(); + } + + private void initializeFailingEdgeListener() { + controller.getEdge().failingProperty().addListener((observable, oldFailing, newFailing) -> onFailingUpdate(controller.getEdge(), newFailing)); + } + + private void onFailingUpdate(DisplayableEdge edge, Boolean isFailing) { + for (Nail nail : edge.getNails()) { + if (nail.getPropertyType().equals(DisplayableEdge.PropertyType.SYNCHRONIZATION)) { + controller.getNailNailPresentationMap().get(nail).onFailingUpdate(isFailing); + } + } } public EdgeController getController() { diff --git a/src/main/java/ecdar/presentations/EditorPresentation.java b/src/main/java/ecdar/presentations/EditorPresentation.java new file mode 100644 index 00000000..d17a4ab0 --- /dev/null +++ b/src/main/java/ecdar/presentations/EditorPresentation.java @@ -0,0 +1,201 @@ +package ecdar.presentations; + +import com.jfoenix.controls.JFXPopup; +import com.jfoenix.controls.JFXRippler; +import ecdar.controllers.EditorController; +import ecdar.utility.UndoRedoStack; +import ecdar.utility.colors.Color; +import ecdar.utility.colors.EnabledColor; +import ecdar.utility.helpers.SelectHelper; +import javafx.animation.FadeTransition; +import javafx.animation.ScaleTransition; +import javafx.collections.ListChangeListener; +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.*; +import javafx.scene.shape.Circle; +import javafx.util.Duration; +import javafx.util.Pair; + +import java.util.ArrayList; +import java.util.List; + +import static ecdar.utility.colors.EnabledColor.enabledColors; + +public class EditorPresentation extends VBox { + private final EditorController controller; + + public EditorPresentation() { + this.controller = new EcdarFXMLLoader().loadAndGetController("EditorPresentation.fxml", this); + initializeToolbar(); + initializeColorSelector(); + + initializeSelectDependentToolbarButton(controller.colorSelected); + Tooltip.install(controller.colorSelected, new Tooltip("Colour")); + + initializeSelectDependentToolbarButton(controller.deleteSelected); + Tooltip.install(controller.deleteSelected, new Tooltip("Delete")); + + initializeToolbarButton(controller.undo); + initializeToolbarButton(controller.redo); + initializeUndoRedoButtons(); + } + + public EditorController getController() { + return controller; + } + + private void initializeSelectDependentToolbarButton(final JFXRippler button) { + initializeToolbarButton(button); + + // The color button should only be enabled when an element is selected + SelectHelper.getSelectedElements().addListener(new ListChangeListener() { + @Override + public void onChanged(final Change c) { + if (SelectHelper.getSelectedElements().size() > 0) { + button.setEnabled(true); + + final FadeTransition fadeAnimation = new FadeTransition(Duration.millis(100), button); + fadeAnimation.setFromValue(button.getOpacity()); + fadeAnimation.setToValue(1); + fadeAnimation.play(); + } else { + button.setEnabled(false); + + final FadeTransition fadeAnimation = new FadeTransition(Duration.millis(100), button); + fadeAnimation.setFromValue(1); + fadeAnimation.setToValue(0.3); + fadeAnimation.play(); + } + } + }); + + // Disable the button + button.setEnabled(false); + button.setOpacity(0.3); + } + + private void initializeToolbarButton(final JFXRippler button) { + final Color color = Color.GREY_BLUE; + final Color.Intensity colorIntensity = Color.Intensity.I800; + + button.setMaskType(JFXRippler.RipplerMask.CIRCLE); + button.setRipplerFill(color.getTextColor(colorIntensity)); + button.setPosition(JFXRippler.RipplerPos.BACK); + } + + private void initializeUndoRedoButtons() { + UndoRedoStack.canUndoProperty().addListener((obs, oldState, newState) -> { + if (newState) { + // Enable the undo button + controller.undo.setEnabled(true); + controller.undo.setOpacity(1); + } else { + // Disable the undo button + controller.undo.setEnabled(false); + controller.undo.setOpacity(0.3); + } + }); + + UndoRedoStack.canRedoProperty().addListener((obs, oldState, newState) -> { + if (newState) { + // Enable the redo button + controller.redo.setEnabled(true); + controller.redo.setOpacity(1); + } else { + // Disable the redo button + controller.redo.setEnabled(false); + controller.redo.setOpacity(0.3); + } + }); + + // Disable the undo button + controller.undo.setEnabled(false); + controller.undo.setOpacity(0.3); + + // Disable the redo button + controller.redo.setEnabled(false); + controller.redo.setOpacity(0.3); + + // Set tooltips + Tooltip.install(controller.undo, new Tooltip("Undo")); + Tooltip.install(controller.redo, new Tooltip("Redo")); + } + + private void initializeColorSelector() { + final JFXPopup popup = new JFXPopup(); + + final double listWidth = 136; + final FlowPane list = new FlowPane(); + for (final EnabledColor color : enabledColors) { + final Circle circle = new Circle(16, color.color.getColor(color.intensity)); + circle.setStroke(color.color.getColor(color.intensity.next(2))); + circle.setStrokeWidth(1); + + final Label label = new Label(color.keyCode.getName()); + label.getStyleClass().add("subhead"); + label.setTextFill(color.color.getTextColor(color.intensity)); + + final StackPane child = new StackPane(circle, label); + child.setMinSize(40, 40); + child.setMaxSize(40, 40); + + child.setOnMouseEntered(event -> { + final ScaleTransition scaleTransition = new ScaleTransition(Duration.millis(100), circle); + scaleTransition.setFromX(circle.getScaleX()); + scaleTransition.setFromY(circle.getScaleY()); + scaleTransition.setToX(1.1); + scaleTransition.setToY(1.1); + scaleTransition.play(); + }); + + child.setOnMouseExited(event -> { + final ScaleTransition scaleTransition = new ScaleTransition(Duration.millis(100), circle); + scaleTransition.setFromX(circle.getScaleX()); + scaleTransition.setFromY(circle.getScaleY()); + scaleTransition.setToX(1.0); + scaleTransition.setToY(1.0); + scaleTransition.play(); + }); + + child.setOnMouseClicked(event -> { + final List> previousColor = new ArrayList<>(); + + SelectHelper.getSelectedElements().forEach(selectable -> { + previousColor.add(new Pair<>(selectable, new EnabledColor(selectable.getColor(), selectable.getColorIntensity()))); + }); + + controller.changeColorOnSelectedElements(color, previousColor); + + popup.hide(); + SelectHelper.clearSelectedElements(); + }); + + list.getChildren().add(child); + } + list.setMinWidth(listWidth); + list.setMaxWidth(listWidth); + list.setStyle("-fx-background-color: white; -fx-padding: 8;"); + + popup.setPopupContent(list); + + controller.colorSelected.setOnMouseClicked((e) -> { + // If nothing is selected + if (SelectHelper.getSelectedElements().size() == 0) return; + popup.show(controller.colorSelected, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -10, 15); + }); + } + + private void initializeToolbar() { + final Color color = Color.GREY_BLUE; + final Color.Intensity intensity = Color.Intensity.I700; + + // Set the background for the top toolbar + controller.toolbar.setBackground( + new Background(new BackgroundFill(color.getColor(intensity), + CornerRadii.EMPTY, + Insets.EMPTY) + )); + } +} diff --git a/src/main/java/ecdar/presentations/FilePresentation.java b/src/main/java/ecdar/presentations/FilePresentation.java index 5f1d1251..700b61b8 100644 --- a/src/main/java/ecdar/presentations/FilePresentation.java +++ b/src/main/java/ecdar/presentations/FilePresentation.java @@ -132,7 +132,7 @@ private void initializeColors() { private ArrayList getActiveComponents() { ArrayList activeComponents = new ArrayList<>(); - Node canvasPaneFirstChild = Ecdar.getPresentation().getController().canvasPane.getChildren().get(0); + Node canvasPaneFirstChild = Ecdar.getPresentation().getController().getEditorPresentation().getController().canvasPane.getChildren().get(0); if(canvasPaneFirstChild instanceof GridPane) { for (Node child : ((GridPane) canvasPaneFirstChild).getChildren()) { activeComponents.add(((CanvasPresentation) child).getController().getActiveModel()); diff --git a/src/main/java/ecdar/presentations/LeftSimPanePresentation.java b/src/main/java/ecdar/presentations/LeftSimPanePresentation.java new file mode 100755 index 00000000..adc693ed --- /dev/null +++ b/src/main/java/ecdar/presentations/LeftSimPanePresentation.java @@ -0,0 +1,37 @@ +package ecdar.presentations; + +import ecdar.controllers.LeftSimPaneController; +import ecdar.utility.colors.Color; +import javafx.geometry.Insets; +import javafx.scene.layout.*; + +public class LeftSimPanePresentation extends StackPane { + private LeftSimPaneController controller; + + public LeftSimPanePresentation() { + controller = new EcdarFXMLLoader().loadAndGetController("LeftSimPanePresentation.fxml", this); + + initializeBackground(); + initializeRightBorder(); + } + + private void initializeBackground() { + controller.scrollPaneVbox.setBackground(new Background(new BackgroundFill( + Color.GREY.getColor(Color.Intensity.I200), + CornerRadii.EMPTY, + Insets.EMPTY + ))); + } + + /** + * Initializes the thin border on the right side of the transition toolbar + */ + private void initializeRightBorder() { + controller.transitionPanePresentation.getController().toolbar.setBorder(new Border(new BorderStroke( + Color.GREY_BLUE.getColor(Color.Intensity.I900), + BorderStrokeStyle.SOLID, + CornerRadii.EMPTY, + new BorderWidths(0, 1, 0, 0) + ))); + } +} diff --git a/src/main/java/ecdar/presentations/Link.java b/src/main/java/ecdar/presentations/Link.java index d4e30147..1c3cc98e 100644 --- a/src/main/java/ecdar/presentations/Link.java +++ b/src/main/java/ecdar/presentations/Link.java @@ -1,9 +1,8 @@ package ecdar.presentations; import ecdar.Debug; -import ecdar.abstractions.EdgeStatus; -import ecdar.utility.colors.Color; import ecdar.utility.Highlightable; +import ecdar.utility.colors.Color; import ecdar.utility.helpers.SelectHelper; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; diff --git a/src/main/java/ecdar/presentations/LocationPresentation.java b/src/main/java/ecdar/presentations/LocationPresentation.java index 8d2a55af..57def2ab 100644 --- a/src/main/java/ecdar/presentations/LocationPresentation.java +++ b/src/main/java/ecdar/presentations/LocationPresentation.java @@ -158,11 +158,17 @@ private void initializeIdLabel() { final ObjectProperty color = location.colorProperty(); final ObjectProperty colorIntensity = location.colorIntensityProperty(); + final BooleanProperty failing = location.failingProperty(); // Delegate to style the label based on the color of the location final BiConsumer updateColor = (newColor, newIntensity) -> { - idLabel.setTextFill(newColor.getTextColor(newIntensity)); - ds.setColor(newColor.getColor(newIntensity)); + if (location.getFailing()) { + idLabel.setTextFill(Color.RED.getTextColor(newIntensity)); + ds.setColor(Color.RED.getColor(newIntensity)); + } else { + idLabel.setTextFill(newColor.getTextColor(newIntensity)); + ds.setColor(newColor.getColor(newIntensity)); + } }; updateColorDelegates.add(updateColor); @@ -172,6 +178,7 @@ private void initializeIdLabel() { // Update the color of the circle when the color of the location is updated color.addListener((obs, old, newColor) -> updateColor.accept(newColor, colorIntensity.get())); + failing.addListener((obs, old, newFailing) -> updateColor.accept(color.get(), colorIntensity.get() )); } private void initializeTags() { @@ -182,6 +189,9 @@ private void initializeTags() { } controller.nicknameTag.replaceSpace(); + controller.nicknameTag.replaceSigns(); + controller.invariantTag.replaceSigns(); + // Set the layout from the model (if they are not both 0) final Location loc = controller.getLocation(); @@ -196,10 +206,15 @@ private void initializeTags() { } // Bind the model to the layout - loc.nicknameXProperty().bindBidirectional(controller.nicknameTag.translateXProperty()); - loc.nicknameYProperty().bindBidirectional(controller.nicknameTag.translateYProperty()); - loc.invariantXProperty().bindBidirectional(controller.invariantTag.translateXProperty()); - loc.invariantYProperty().bindBidirectional(controller.invariantTag.translateYProperty()); + // Check is needed because the property cannot be bound twice + // which happens when switching from the simulator to the editor + if (!loc.nicknameXProperty().isBound() && !loc.nicknameYProperty().isBound() && + !loc.invariantXProperty().isBound() && !loc.invariantYProperty().isBound()) { + loc.nicknameXProperty().bindBidirectional(controller.nicknameTag.translateXProperty()); + loc.nicknameYProperty().bindBidirectional(controller.nicknameTag.translateYProperty()); + loc.invariantXProperty().bindBidirectional(controller.invariantTag.translateXProperty()); + loc.invariantYProperty().bindBidirectional(controller.invariantTag.translateYProperty()); + } final Consumer updateTags = location -> { // Update the color @@ -405,17 +420,35 @@ protected void interpolate(final double frac) { // Update the colors final ObjectProperty color = location.colorProperty(); final ObjectProperty colorIntensity = location.colorIntensityProperty(); + final BooleanProperty failing = location.failingProperty(); // Delegate to style the label based on the color of the location final BiConsumer updateColor = (newColor, newIntensity) -> { - notCommittedShape.setFill(newColor.getColor(newIntensity)); if (!location.getUrgency().equals(Location.Urgency.PROHIBITED)) { - notCommittedShape.setStroke(newColor.getColor(newIntensity.next(2))); + if (location.getFailing()) { + notCommittedShape.setStroke(Color.RED.getColor(newIntensity)); + } else { + notCommittedShape.setStroke(newColor.getColor(newIntensity.next(2))); + } } + if (location.getFailing()) { + notCommittedShape.setFill(Color.RED.getColor(newIntensity)); + committedShape.setFill(Color.RED.getColor(newIntensity)); + committedShape.setStroke(Color.RED.getColor(newIntensity.next(2))); + } else { + notCommittedShape.setFill(newColor.getColor(newIntensity)); + committedShape.setFill(newColor.getColor(newIntensity)); + committedShape.setStroke(newColor.getColor(newIntensity.next(2))); + } + }; - committedShape.setFill(newColor.getColor(newIntensity)); - committedShape.setStroke(newColor.getColor(newIntensity.next(2))); + final Consumer handleFailingUpdate = (isFailing) -> { + if(isFailing) { + updateColor.accept(Color.RED, colorIntensity.get()); + } else { + updateColor.accept(location.getColor(), colorIntensity.get()); + } }; updateColorDelegates.add(updateColor); @@ -425,6 +458,7 @@ protected void interpolate(final double frac) { // Update the color of the circle when the color of the location is updated color.addListener((obs, old, newColor) -> updateColor.accept(newColor, colorIntensity.get())); + failing.addListener((obs, old, newFailing) -> updateColor.accept(color.get(), colorIntensity.get())); } private void initializeTypeGraphics() { diff --git a/src/main/java/ecdar/presentations/MenuElement.java b/src/main/java/ecdar/presentations/MenuElement.java index 8a145e1e..3094fdeb 100644 --- a/src/main/java/ecdar/presentations/MenuElement.java +++ b/src/main/java/ecdar/presentations/MenuElement.java @@ -1,12 +1,8 @@ package ecdar.presentations; -import ecdar.Ecdar; -import ecdar.controllers.EcdarController; import ecdar.utility.colors.Color; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableBooleanValue; -import javafx.css.CssMetaData; -import javafx.css.Styleable; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.Label; @@ -14,15 +10,13 @@ import javafx.scene.layout.*; import org.kordamp.ikonli.javafx.FontIcon; - -import java.util.List; import java.util.function.Consumer; import static javafx.scene.paint.Color.TRANSPARENT; import static javafx.scene.paint.Color.WHITE; -/** - * Represents an element of the dropdown menu, excluding spacer and the colour palette element. + +/* Represents an element of the dropdown menu, excluding spacer and the colour palette element. */ public class MenuElement { public static final javafx.scene.paint.Color DISABLED_COLOR = Color.GREY_BLUE.getColor(Color.Intensity.I300); @@ -110,7 +104,7 @@ public MenuElement(final String s, final String iconString, final Consumer mouseEventConsumer){ rippler = new ReleaseRippler(clickListenerFix); - rippler.setRipplerFill(javafx.scene.paint.Color.TRANSPARENT); + rippler.setRipplerFill(TRANSPARENT); rippler.setOnMouseEntered(event -> { if (isDisabled.get()) return; diff --git a/src/main/java/ecdar/presentations/MessageTabPanePresentation.java b/src/main/java/ecdar/presentations/MessageTabPanePresentation.java deleted file mode 100644 index 88c8dda8..00000000 --- a/src/main/java/ecdar/presentations/MessageTabPanePresentation.java +++ /dev/null @@ -1,115 +0,0 @@ -package ecdar.presentations; - -import ecdar.code_analysis.CodeAnalysis; -import ecdar.controllers.MessageTabPaneController; -import javafx.beans.InvalidationListener; -import javafx.beans.Observable; -import javafx.scene.Cursor; -import javafx.scene.layout.StackPane; - -public class MessageTabPanePresentation extends StackPane { - private final MessageTabPaneController controller; - - public MessageTabPanePresentation() { - controller = new EcdarFXMLLoader().loadAndGetController("MessageTabPanePresentation.fxml", this); - initializeMessageContainer(); - } - - public MessageTabPaneController getController() { - return controller; - } - - private void initializeMessageContainer() { - // The element of which you drag to resize should be equal to the width of the window (main stage) - controller.tabPaneResizeElement.sceneProperty().addListener((observableScene, oldScene, newScene) -> { - if (oldScene == null && newScene != null) { - // scene is set for the first time. Now its the time to listen stage changes. - newScene.windowProperty().addListener((observableWindow, oldWindow, newWindow) -> { - if (oldWindow == null && newWindow != null) { - newWindow.widthProperty().addListener((observableWidth, oldWidth, newWidth) -> { - controller.tabPaneResizeElement.setWidth(newWidth.doubleValue() - 30); - }); - } - }); - } - }); - - // Resize cursor - controller.tabPaneResizeElement.setCursor(Cursor.N_RESIZE); - - // Remove the background of the scroll panes - controller.errorsScrollPane.setStyle("-fx-background-color: transparent;"); - controller.warningsScrollPane.setStyle("-fx-background-color: transparent;"); - - final Runnable collapseIfNoErrorsOrWarnings = () -> { - new Thread(() -> { - // Wait for a second to check if new warnings or errors occur - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - // Check if any warnings or errors occurred - if (CodeAnalysis.getBackendErrors().size() + CodeAnalysis.getErrors().size() + CodeAnalysis.getWarnings().size() == 0) { - controller.collapseMessagesIfNotCollapsed(); - } - }).start(); - }; - - // Update the tab-text and expand/collapse the view - CodeAnalysis.getBackendErrors().addListener(new InvalidationListener() { - @Override - public void invalidated(final Observable observable) { - final int errors = CodeAnalysis.getBackendErrors().size(); - if (errors == 0) { - controller.backendErrorsTab.setText("Backend Errors"); - } else { - controller.backendErrorsTab.setText("Backend Errors (" + errors + ")"); - controller.expandMessagesIfNotExpanded(); - controller.tabPane.getSelectionModel().select(controller.backendErrorsTab); - } - - collapseIfNoErrorsOrWarnings.run(); - } - }); - - // Update the tab-text and expand/collapse the view - CodeAnalysis.getErrors().addListener(new InvalidationListener() { - @Override - public void invalidated(final Observable observable) { - final int errors = CodeAnalysis.getErrors().size(); - if (errors == 0) { - controller.errorsTab.setText("Errors"); - } else { - controller.errorsTab.setText("Errors (" + errors + ")"); - controller.expandMessagesIfNotExpanded(); - controller.tabPane.getSelectionModel().select(controller.errorsTab); - } - - collapseIfNoErrorsOrWarnings.run(); - } - }); - - - // Update the tab-text and expand/collapse the view - CodeAnalysis.getWarnings().addListener(new InvalidationListener() { - @Override - public void invalidated(final Observable observable) { - final int warnings = CodeAnalysis.getWarnings().size(); - if (warnings == 0) { - controller.warningsTab.setText("Warnings"); - } else { - controller.warningsTab.setText("Warnings (" + warnings + ")"); - //We must select the warnings tab but we don't want the messages areas to open - controller.shouldISkipOpeningTheMessagesContainer = true; - controller.tabPane.getSelectionModel().select(controller.warningsTab); - } - - collapseIfNoErrorsOrWarnings.run(); - } - }); - - controller.collapseMessagesIcon.getStyleClass().add("icon-size-medium"); - } -} \ No newline at end of file diff --git a/src/main/java/ecdar/presentations/MultiSyncTagPresentation.java b/src/main/java/ecdar/presentations/MultiSyncTagPresentation.java index c5929f93..e3f86927 100644 --- a/src/main/java/ecdar/presentations/MultiSyncTagPresentation.java +++ b/src/main/java/ecdar/presentations/MultiSyncTagPresentation.java @@ -4,7 +4,6 @@ import ecdar.abstractions.DisplayableEdge; import ecdar.abstractions.Edge; import ecdar.abstractions.GroupedEdge; -import ecdar.abstractions.Nail; import ecdar.controllers.EcdarController; import ecdar.controllers.MultiSyncTagController; import ecdar.utility.UndoRedoStack; diff --git a/src/main/java/ecdar/presentations/NailPresentation.java b/src/main/java/ecdar/presentations/NailPresentation.java index 2ab5823f..41913c2a 100644 --- a/src/main/java/ecdar/presentations/NailPresentation.java +++ b/src/main/java/ecdar/presentations/NailPresentation.java @@ -12,6 +12,7 @@ import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.application.Platform; +import javafx.beans.value.ObservableValue; import javafx.scene.Group; import javafx.scene.control.Label; import javafx.scene.shape.Line; @@ -51,6 +52,7 @@ public NailPresentation(final Nail nail, final DisplayableEdge edge, final Compo } controller.propertyTag.setTranslateX(10); + controller.propertyTag.replaceSigns(); controller.propertyTag.setTranslateY(-controller.propertyTag.getHeight()); this.getChildren().add(controller.propertyTag); @@ -74,7 +76,6 @@ private void initializeRadius() { radiusUpdater.accept(controller.getNail().getPropertyType()); } - private void initializePropertyTag() { final TagPresentation propertyTag = controller.propertyTag; final Line propertyTagLine = controller.propertyTagLine; @@ -96,9 +97,14 @@ private void initializePropertyTag() { propertyTag.setTranslateX(controller.getNail().getPropertyX()); propertyTag.setTranslateY(controller.getNail().getPropertyY()); } - controller.getNail().propertyXProperty().bindBidirectional(propertyTag.translateXProperty()); - controller.getNail().propertyYProperty().bindBidirectional(propertyTag.translateYProperty()); + // Check is needed because the property cannot be bound twice + // which happens when switching from the simulator to the editor + if (!controller.getNail().propertyXProperty().isBound() && !controller.getNail().propertyYProperty().isBound()) { + controller.getNail().propertyXProperty().bindBidirectional(propertyTag.translateXProperty()); + controller.getNail().propertyYProperty().bindBidirectional(propertyTag.translateYProperty()); + } + Label propertyLabel = controller.propertyLabel; if(propertyType.equals(Edge.PropertyType.SELECTION)) { @@ -181,11 +187,44 @@ private void updateSyncLabel(final TagPresentation propertyTag) { } private void initializeNailCircleColor() { - final Runnable updateNailColor = () -> { + + // When the color of the component updates, update the nail indicator as well + controller.getComponent().colorProperty().addListener((observable) -> updateNailColor()); + + // When the color intensity of the component updates, update the nail indicator + controller.getComponent().colorIntensityProperty().addListener((observable) -> updateNailColor()); + // Initialize the color of the nail + updateNailColor(); + } + + /** + * Update color when the edge of this nails failing property is updated. + */ + public void onFailingUpdate(boolean isFailing) { + final Runnable updateNailColorOnFailingUpdate = () -> { final Color color = controller.getComponent().getColor(); final Color.Intensity colorIntensity = controller.getComponent().getColorIntensity(); + controller.nailCircle.setFill(Color.RED.getColor(colorIntensity)); + controller.nailCircle.setStroke(Color.RED.getColor(colorIntensity.next(2))); + }; + if (isFailing) { + updateNailColorOnFailingUpdate.run(); + } else { + updateNailColor(); + } + } - if(!controller.getNail().getPropertyType().equals(Edge.PropertyType.NONE)) { + private void updateNailColor() { + final Runnable updateNailColor = () -> { + final Color color = controller.getComponent().getColor(); + final Color.Intensity colorIntensity = controller.getComponent().getColorIntensity(); + //If edge is failing and is a SYNC + if (controller.getEdge().getFailing() && controller.getNail().getPropertyType().equals(Edge.PropertyType.SYNCHRONIZATION)) { + controller.nailCircle.setFill(Color.RED.getColor(colorIntensity)); + controller.nailCircle.setStroke(Color.RED.getColor(colorIntensity.next(2))); + } + //If edge is not NONE + else if (!controller.getNail().getPropertyType().equals(Edge.PropertyType.NONE)) { controller.nailCircle.setFill(color.getColor(colorIntensity)); controller.nailCircle.setStroke(color.getColor(colorIntensity.next(2))); } else { @@ -193,14 +232,6 @@ private void initializeNailCircleColor() { controller.nailCircle.setStroke(Color.GREY_BLUE.getColor(Color.Intensity.I900)); } }; - - // When the color of the component updates, update the nail indicator as well - controller.getComponent().colorProperty().addListener((observable) -> updateNailColor.run()); - - // When the color intensity of the component updates, update the nail indicator - controller.getComponent().colorIntensityProperty().addListener((observable) -> updateNailColor.run()); - - // Initialize the color of the nail updateNailColor.run(); } @@ -237,17 +268,7 @@ public void select() { @Override public void deselect() { - Color color = Color.GREY_BLUE; - Color.Intensity intensity = Color.Intensity.I800; - - // Set the color - if(!controller.getNail().getPropertyType().equals(Edge.PropertyType.NONE)) { - color = controller.getComponent().getColor(); - intensity = controller.getComponent().getColorIntensity(); - } - - controller.nailCircle.setFill(color.getColor(intensity)); - controller.nailCircle.setStroke(color.getColor(intensity.next(2))); + updateNailColor(); } public NailController getController() { diff --git a/src/main/java/ecdar/presentations/ProcessPresentation.java b/src/main/java/ecdar/presentations/ProcessPresentation.java new file mode 100755 index 00000000..51cebab0 --- /dev/null +++ b/src/main/java/ecdar/presentations/ProcessPresentation.java @@ -0,0 +1,282 @@ +package ecdar.presentations; + +import ecdar.abstractions.Component; +import ecdar.abstractions.Edge; +import ecdar.abstractions.Location; +import ecdar.abstractions.Nail; +import ecdar.controllers.ModelController; +import ecdar.controllers.ProcessController; +import ecdar.utility.colors.Color; +import javafx.beans.InvalidationListener; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Cursor; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.*; +import javafx.scene.paint.Paint; +import javafx.scene.shape.Path; +import javafx.scene.shape.Rectangle; +import javafx.scene.shape.Shape; +import javafx.scene.text.Font; + +import java.math.BigDecimal; +import java.util.List; +import java.util.function.BiConsumer; + +import static ecdar.presentations.Grid.GRID_SIZE; + +/** + * The presenter of a Process which is shown in {@link SimulatorOverviewPresentation}.
+ * This class have some of the same functionality as {@link ComponentPresentation} and could be refactored + * into a base class. + */ +public class ProcessPresentation extends ModelPresentation { + private final ProcessController controller; + + /** + * Constructs a Process ready to go to the view. + * @param component the component which the process should look like + */ + public ProcessPresentation(final Component component){ + controller = new EcdarFXMLLoader().loadAndGetController("ProcessPresentation.fxml", this); + controller.setComponent(component); + super.initialize(component.getBox()); + // Initialize methods that is sensitive to width and height + final Runnable onUpdateSize = () -> { + initializeToolbar(); + initializeFrame(); + initializeBackground(); + }; + + onUpdateSize.run(); + + // Re run initialisation on update of width and height property + component.getBox().getWidthProperty().addListener(observable -> onUpdateSize.run()); + component.getBox().getHeightProperty().addListener(observable -> onUpdateSize.run()); + setValueAreaStyle(); + setToggleValueButtonStyle(); + + controller.getClocks().forEach(this::addValueToValueArea); + controller.getVariables().forEach(this::addValueToValueArea); + + controller.getClocks().addListener((InvalidationListener) obs -> { + controller.getClocks().forEach((s, bigDecimal) -> { + final List filteredList = controller.valueArea.getChildren().filtered(node -> { + if (!(node instanceof Label))// we currently only want to look at labels + return false; + final String[] splitString = ((Label) node).getText().split("="); + return splitString[0].trim().equals(s); + }); + controller.valueArea.getChildren().removeAll(filteredList); + addValueToValueArea(s, bigDecimal); + }); + }); + controller.getVariables().addListener((InvalidationListener) obs -> { + controller.getVariables().forEach((s, bigDecimal) -> { + final List filteredList = controller.valueArea.getChildren().filtered(node -> { + if (!(node instanceof Label))// we currently only want to look at labels + return false; + final String[] splitString = ((Label) node).getText().split("="); + return splitString[0].trim().equals(s); + }); + controller.valueArea.getChildren().removeAll(filteredList); + addValueToValueArea(s, bigDecimal); + }); + }); + } + + /** + * Sets the Icon and Icon size of the {@link ProcessController#toggleValueButtonIcon} + */ + private void setToggleValueButtonStyle() { + controller.toggleValueButtonIcon.setIconLiteral("gmi-code"); + controller.toggleValueButtonIcon.setIconSize(17); + } + + /** + * Set the style needed for the {@link ProcessController#valueArea}. + * This include padding and background. + */ + private void setValueAreaStyle() { + // As for some reason the styling fail to be applied we need to set the styling again here + controller.valueArea.setPadding(new Insets(16, 4, 16, 4)); + controller.valueArea.setBackground(new Background( + new BackgroundFill(Paint.valueOf("rgba(242, 243, 244, 0.85)"),CornerRadii.EMPTY, Insets.EMPTY))); + } + + private void addValueToValueArea(final String s, final BigDecimal bigDecimal) { + final Label valueLabel = new Label(); + valueLabel.setText(s + " = " + bigDecimal); + // As the styling sometimes are gone missing + valueLabel.setFont(Font.font("Roboto Mono Medium",13)); + controller.valueArea.getChildren().add(valueLabel); + } + + /** + * Initializes the frame around the body of the process. + * It also updates the color if the color is changed + */ + private void initializeFrame() { + final Component component = controller.getComponent(); + + final Shape[] mask = new Shape[1]; + final Rectangle rectangle = new Rectangle(component.getBox().getWidth(), component.getBox().getHeight()); + + final BiConsumer updateColor = (newColor, newIntensity) -> { + // Mask the parent of the frame (will also mask the background) + mask[0] = Path.subtract(rectangle, TOP_LEFT_CORNER); + controller.frame.setClip(mask[0]); + controller.background.setClip(Path.union(mask[0], mask[0])); + controller.background.setOpacity(0.5); + + // Bind the missing lines that we cropped away + controller.topLeftLine.setStartX(Grid.CORNER_SIZE); + controller.topLeftLine.setStartY(0); + controller.topLeftLine.setEndX(0); + controller.topLeftLine.setEndY(Grid.CORNER_SIZE); + controller.topLeftLine.setStroke(newColor.getColor(newIntensity.next(2))); + controller.topLeftLine.setStrokeWidth(1.25); + StackPane.setAlignment(controller.topLeftLine, Pos.TOP_LEFT); + + // Set the stroke color to two shades darker + controller.frame.setBorder(new Border(new BorderStroke( + newColor.getColor(newIntensity.next(2)), + BorderStrokeStyle.SOLID, + CornerRadii.EMPTY, + new BorderWidths(1), + Insets.EMPTY + ))); + }; + component.colorProperty().addListener(observable -> { + updateColor.accept(component.getColor(), component.getColorIntensity()); + }); + updateColor.accept(component.getColor(), component.getColorIntensity()); + } + + /** + * Initializes the background of the process with the right color + * If the color is changed it will also update it self + */ + private void initializeBackground() { + final Component component = controller.getComponent(); + + // Bind the background width and height to the values in the model + controller.background.widthProperty().bind(component.getBox().getWidthProperty()); + controller.background.heightProperty().bind(component.getBox().getHeightProperty()); + controller.background.setFill(component.getColor().getColor(component.getColorIntensity().next(-10).next(2))); + final BiConsumer updateColor = (newColor, newIntensity) -> { + // Set the background color to the lightest possible version of the color + controller.background.setFill(newColor.getColor(newIntensity.next(-10).next(2))); + }; + component.colorProperty().addListener(observable -> { + updateColor.accept(component.getColor(), component.getColorIntensity()); + }); + + updateColor.accept(component.getColor(), component.getColorIntensity()); + } + + /** + * Initialize the Toolbar of the process.
+ * The toolbar is where the {@link ProcessController#name} is placed. + */ + private void initializeToolbar() { + final Component component = controller.getComponent(); + + final BiConsumer updateColor = (newColor, newIntensity) -> { + // Set the background of the toolbar + controller.toolbar.setBackground(new Background(new BackgroundFill( + newColor.getColor(newIntensity), + CornerRadii.EMPTY, + Insets.EMPTY + ))); + + // Set the icon color and rippler color of the toggleDeclarationButton + controller.toggleValuesButton.setRipplerFill(newColor.getTextColor(newIntensity)); + + controller.toolbar.setPrefHeight(Grid.TOOL_BAR_HEIGHT); + controller.toggleValuesButton.setBackground(Background.EMPTY); + }; + controller.getComponent().colorProperty().addListener(observable -> updateColor.accept(component.getColor(), component.getColorIntensity())); + + updateColor.accept(component.getColor(), component.getColorIntensity()); + + // Set a hover effect for the controller.toggleDeclarationButton + controller.toggleValuesButton.setOnMouseEntered(event -> controller.toggleValuesButton.setCursor(Cursor.HAND)); + controller.toggleValuesButton.setOnMouseExited(event -> controller.toggleValuesButton.setCursor(Cursor.DEFAULT)); + } + + /** + * Fades the process. + * Used if it is not involved in a transition + */ + public void showInactive() { + setOpacity(0.5); + } + + /** + * Show the process as active. + * Used to reset the effect of {@link ProcessPresentation#showInactive()} + */ + public void showActive() { + setOpacity(1.0); + } + + @Override + ModelController getModelController() { + return controller; + } + + /** + * Gets the minimum possible width when dragging the anchor. + * The width is based on the x coordinate of locations, nails and the signature arrows.
+ * This should be removed from {@link ModelPresentation} and made into an interface of its own + * @return the minimum possible width. + */ + @Override + @Deprecated + double getDragAnchorMinWidth() { + final Component component = controller.getComponent(); + double minWidth = 10 * GRID_SIZE; + + for (final Location location : component.getLocations()) { + minWidth = Math.max(minWidth, location.getX() + GRID_SIZE * 2); + } + + for (final Edge edge : component.getEdges()) { + for (final Nail nail : edge.getNails()) { + minWidth = Math.max(minWidth, nail.getX() + GRID_SIZE); + } + } + return minWidth; + } + + /** + * Gets the minimum possible height when dragging the anchor. + * The height is based on the y coordinate of locations, nails and the signature arrows
+ * This should be removed from {@link ModelPresentation} and made into an interface of its own + * @return the minimum possible height. + */ + @Override + @Deprecated + double getDragAnchorMinHeight() { + final Component component = controller.getComponent(); + double minHeight = 10 * GRID_SIZE; + + for (final Location location : component.getLocations()) { + minHeight = Math.max(minHeight, location.getY() + GRID_SIZE * 2); + } + + for (final Edge edge : component.getEdges()) { + for (final Nail nail : edge.getNails()) { + minHeight = Math.max(minHeight, nail.getY() + GRID_SIZE); + } + } + + return minHeight; + } + + public ProcessController getController() { + return controller; + } +} diff --git a/src/main/java/ecdar/presentations/QueryPresentation.java b/src/main/java/ecdar/presentations/QueryPresentation.java index db1d9179..852b1704 100644 --- a/src/main/java/ecdar/presentations/QueryPresentation.java +++ b/src/main/java/ecdar/presentations/QueryPresentation.java @@ -7,19 +7,18 @@ import ecdar.controllers.QueryController; import ecdar.controllers.EcdarController; import ecdar.utility.colors.Color; +import ecdar.utility.helpers.StringHelper; import ecdar.utility.helpers.StringValidator; import javafx.application.Platform; import javafx.beans.binding.When; import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Insets; -import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.control.Label; -import javafx.scene.control.TitledPane; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCode; import javafx.scene.layout.*; -import javafx.scene.text.TextAlignment; +import javafx.geometry.Point2D; import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.material.Material; @@ -43,7 +42,6 @@ public QueryPresentation(final Query query) { initializeActionButton(); initializeDetailsButton(); initializeTextFields(); - initializeInputOutputPaneAndAddIgnoredInputOutputs(); initializeMoreInformationButtonAndQueryTypeSymbol(); initializeBackendsDropdown(); @@ -73,7 +71,7 @@ private void initializeTextFields() { queryTextField.setOnKeyPressed(EcdarController.getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler(keyEvent -> { Platform.runLater(() -> { - if (keyEvent.getCode().equals(KeyCode.ENTER)) { + if (keyEvent.getCode().equals(KeyCode.ENTER) && controller.getQuery().getType() != null) { runQuery(); } }); @@ -196,7 +194,11 @@ private void initializeStateIndicator() { } else { this.tooltip.setText("The component has been created (can be accessed in the project pane)"); } - } else if (queryState.getStatusCode() == 3) { + } + else if (queryState.getStatusCode() == 2){ + this.tooltip.setText("This query was not a success: " + controller.getQuery().getCurrentErrors()); + } + else if (queryState.getStatusCode() == 3) { this.tooltip.setText("The query has not been executed yet"); } else { this.tooltip.setText(controller.getQuery().getCurrentErrors()); @@ -252,9 +254,10 @@ private void initializeStateIndicator() { statusIcon.setOnMouseClicked(event -> { if (controller.getQuery().getQuery().isEmpty()) return; - + Label label = new Label(tooltip.getText()); - JFXDialog dialog = new InformationDialogPresentation("Result from query: " + controller.getQuery().getQuery(), label); + + JFXDialog dialog = new InformationDialogPresentation("Result from query: " + StringHelper.ConvertSymbolsToUnicode(controller.getQuery().getQuery()), label); dialog.show(Ecdar.getPresentation()); }); }); @@ -270,197 +273,6 @@ private void setStatusIndicatorContentColor(javafx.scene.paint.Color color, Font } } - private void initializeInputOutputPaneAndAddIgnoredInputOutputs() { - Platform.runLater(() -> { - final TitledPane inputOutputPane = (TitledPane) lookup("#inputOutputPane"); - inputOutputPane.setAnimated(true); - final Runnable changeTitledPaneVisibility = () -> updateTitlePaneVisibility(inputOutputPane); - - // Run the consumer to ensure that the input/output pane is displayed for existing refinement queries - changeTitledPaneVisibility.run(); - - // Bind the expand icon to the expand property of the pane - inputOutputPane.expandedProperty().addListener((observable, oldValue, newValue) -> { - FontIcon expandIcon = (FontIcon) inputOutputPane.lookup("#inputOutputPaneExpandIcon"); - if (!newValue) { - expandIcon.setIconLiteral("gmi-keyboard-arrow-down"); - } else { - expandIcon.setIconLiteral("gmi-keyboard-arrow-up"); - } - }); - - // Make sure the input/output pane state is updated whenever the query text field loses focus - final JFXTextField queryTextField = (JFXTextField) lookup("#query"); - queryTextField.focusedProperty().addListener((observable, oldValue, newValue) -> { - if (!newValue) { - changeTitledPaneVisibility.run(); - } - }); - - // Change visibility of input/output Pane when backend is changed for the query ToDo NIELS - // lookup("#swapBackendButton").setOnMousePressed(event -> changeTitledPaneVisibility.accept(controller.getQuery().getQuery())); - Platform.runLater(() -> addIgnoredInputOutputsFromQuery(inputOutputPane)); - }); - } - - private void initiateResetInputOutputButton(TitledPane inputOutputPane) { - Platform.runLater(() -> { - final JFXRippler resetInputOutputPaneButton = (JFXRippler) inputOutputPane.lookup("#inputOutputPaneUpdateButton"); - - initializeResetInputOutputPaneButton(inputOutputPane, resetInputOutputPaneButton); - - // Get the inputs and outputs automatically, when executing a refinement query - controller.actionButton.setOnMousePressed(event -> { - // Update the ignored inputs and outputs without clearing the lists - updateInputOutputs(inputOutputPane, false); - }); - - // Install tooltip on the reset button - final Tooltip buttonTooltip = new Tooltip("Refresh inputs and outputs (resets selections)"); - buttonTooltip.setWrapText(true); - Tooltip.install(resetInputOutputPaneButton, buttonTooltip); - }); - } - - private void initializeResetInputOutputPaneButton(TitledPane inputOutputPane, - JFXRippler resetInputOutputPaneButton) { - Platform.runLater(() -> { - final FontIcon resetInputOutputPaneButtonIcon = (FontIcon) lookup("#inputOutputPaneUpdateButtonIcon"); - final JFXSpinner progressIndicator = (JFXSpinner) lookup("#inputOutputProgressIndicator"); - - progressIndicator.setVisible(false); - - // Set the initial state of the reset button - resetInputOutputPaneButton.setCursor(Cursor.HAND); - resetInputOutputPaneButton.setRipplerFill(Color.GREY.getColor(Color.Intensity.I500)); - resetInputOutputPaneButton.setMaskType(JFXRippler.RipplerMask.CIRCLE); - resetInputOutputPaneButtonIcon.setIconColor(Color.GREY.getColor(Color.Intensity.I900)); - - resetInputOutputPaneButton.setOnMousePressed(event -> { - // Disable the button on click - progressIndicator.setVisible(true); - resetInputOutputPaneButton.setDisable(true); - resetInputOutputPaneButtonIcon.setIconColor(Color.GREY.getColor(Color.Intensity.I700)); - - updateInputOutputs(inputOutputPane, true); - - // Enable the button after inputs and outputs have been updated - progressIndicator.setVisible(false); - resetInputOutputPaneButton.setDisable(false); - resetInputOutputPaneButtonIcon.setIconColor(Color.GREY.getColor(Color.Intensity.I900)); - }); - }); - } - - private void updateTitlePaneVisibility(TitledPane inputOutputPane) { - // Check if the query is a refinement and that the engine is set to Reveaal - if (controller.getQuery().getQuery().startsWith("refinement") && BackendHelper.backendSupportsInputOutputs(controller.getQuery().getBackend())) { - initiateResetInputOutputButton(inputOutputPane); - - // Make the input/output pane visible - inputOutputPaneVisibility(true); - } else { - inputOutputPaneVisibility(false); - } - } - - private void updateInputOutputs(TitledPane inputOutputPane, Boolean shouldResetSelections) { - final VBox inputBox = (VBox) inputOutputPane.lookup("#inputBox"); - final VBox outputBox = (VBox) inputOutputPane.lookup("#outputBox"); - - IgnoredInputOutputQuery query = new IgnoredInputOutputQuery(this.controller.getQuery(), this, controller.getQuery().ignoredInputs, inputBox, controller.getQuery().ignoredOutputs, outputBox); - - if (shouldResetSelections) { - // Reset selections for ignored inputs and outputs - clearIgnoredInputsAndOutputs(inputBox, outputBox); - } - - Ecdar.getBackendDriver().getInputOutputs(query, controller.getQuery().getBackend()); - } - - private void clearIgnoredInputsAndOutputs(VBox inputBox, VBox outputBox) { - controller.getQuery().ignoredInputs.clear(); - controller.getQuery().ignoredOutputs.clear(); - - Platform.runLater(() -> { - inputBox.getChildren().clear(); - outputBox.getChildren().clear(); - }); - } - - public void addInputOrOutput(String name, Boolean state, Map associatedMap, VBox associatedBox) { - HBox sliderBox = new HBox(); - sliderBox.setAlignment(Pos.CENTER_LEFT); - - Label label = new Label(name); - label.setWrapText(true); - - // Initialize the toggle slider - JFXToggleButton slider = new JFXToggleButton(); - slider.setStyle("-jfx-toggle-color:#dddddd; -jfx-untoggle-color:#dddddd; -jfx-toggle-line-color:#" + Color.YELLOW.getColor(Color.Intensity.I700).toString().substring(2, 8) + ";-jfx-untoggle-line-color:#" + Color.GREY.getColor(Color.Intensity.I400).toString().substring(2, 8) + "; -fx-padding: 0 0 0 0;"); - slider.setSelected(state); - - // Add label beneath toggle slider to display state - Label stateLabel = new Label(); - stateLabel.setText(state ? "Ignored" : "Included"); - stateLabel.setTextAlignment(TextAlignment.CENTER); - - // Enforce changes of the slider - slider.setOnMouseClicked((event) -> { - associatedMap.replace(name, slider.isSelected()); - stateLabel.setText(slider.isSelected() ? "Ignored" : "Included"); - }); - - // Add toggle slider and state label to VBox and set its width - VBox sliderAndStateLabel = new VBox(); - sliderAndStateLabel.setMinWidth(64); - sliderAndStateLabel.setMaxWidth(64); - sliderAndStateLabel.setSpacing(-7.5); - sliderAndStateLabel.getChildren().addAll(slider, stateLabel); - - // Horizontal space to ensure that the toggle slider and input/output label is not intertwined - Region horizontalSpace = new Region(); - horizontalSpace.setMinWidth(16); - horizontalSpace.setMaxWidth(16); - - sliderBox.getChildren().addAll(sliderAndStateLabel, horizontalSpace, label); - - Platform.runLater(() -> { - // Add checkbox to the scene - associatedBox.getChildren().add(sliderBox); - }); - } - - private void inputOutputPaneVisibility(Boolean visibility) { - Platform.runLater(() -> { - final TitledPane inputOutputPane = (TitledPane) lookup("#inputOutputPane"); - - // Hide/show the inputOutputPane and remove/add the space it would occupy respectively - inputOutputPane.setVisible(visibility); - inputOutputPane.setManaged(visibility); - - // Set expand property only on visibility false to avoid auto expand - if (!visibility) { - inputOutputPane.setExpanded(false); - } - }); - } - - private void addIgnoredInputOutputsFromQuery(TitledPane inputOutputPane) { - final VBox inputBox = (VBox) inputOutputPane.lookup("#inputBox"); - final VBox outputBox = (VBox) inputOutputPane.lookup("#outputBox"); - - // Add inputs as toggles in the GUI - for (Map.Entry entry : controller.getQuery().ignoredInputs.entrySet()) { - addInputOrOutput(entry.getKey(), entry.getValue(), controller.getQuery().ignoredInputs, inputBox); - } - - // Add outputs as toggles in the GUI - for (Map.Entry entry : controller.getQuery().ignoredOutputs.entrySet()) { - addInputOrOutput(entry.getKey(), entry.getValue(), controller.getQuery().ignoredOutputs, outputBox); - } - } - private void initializeMoreInformationButtonAndQueryTypeSymbol() { Platform.runLater(() -> { controller.queryTypeExpand.setVisible(true); @@ -469,7 +281,6 @@ private void initializeMoreInformationButtonAndQueryTypeSymbol() { controller.queryTypeExpand.setRipplerFill(Color.GREY_BLUE.getColor(Color.Intensity.I500)); final DropDownMenu queryTypeDropDown = new DropDownMenu(controller.queryTypeExpand); - queryTypeDropDown.addListElement("Query Type"); QueryType[] queryTypes = QueryType.values(); for (QueryType type : queryTypes) { @@ -478,7 +289,20 @@ private void initializeMoreInformationButtonAndQueryTypeSymbol() { controller.queryTypeExpand.setOnMousePressed((e) -> { e.consume(); - queryTypeDropDown.show(JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, 16, 16); + //height of app window + double windowHeight = this.getScene().getHeight(); + //Location of dropdown relative to the app window + Point2D Origin = this.localToScene(this.getWidth(), this.getHeight()); + //Generate the popups properties before displaying + queryTypeDropDown.show(this); + //Check if the dropdown can fit the app window. + if(Origin.getY()+queryTypeDropDown.getHeight() >= windowHeight){ + queryTypeDropDown.show(JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.RIGHT, -55,-(Origin.getY()-windowHeight)-50); + } + else{ + queryTypeDropDown.show(JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 16, 16); + } + }); controller.queryTypeSymbol.setText(controller.getQuery() != null && controller.getQuery().getType() != null ? controller.getQuery().getType().getSymbol() : "---"); @@ -508,6 +332,6 @@ private void addQueryTypeListElement(final QueryType type, final DropDownMenu dr } private void runQuery() { - controller.getQuery().run(); + Ecdar.getQueryExecutor().executeQuery(this.controller.getQuery()); } } diff --git a/src/main/java/ecdar/presentations/RightSimPanePresentation.java b/src/main/java/ecdar/presentations/RightSimPanePresentation.java new file mode 100755 index 00000000..2345664d --- /dev/null +++ b/src/main/java/ecdar/presentations/RightSimPanePresentation.java @@ -0,0 +1,44 @@ +package ecdar.presentations; + +import ecdar.controllers.RightSimPaneController; +import ecdar.utility.colors.Color; +import javafx.geometry.Insets; +import javafx.scene.layout.*; + +/** + * Presentation class for the right pane in the simulator + */ +public class RightSimPanePresentation extends StackPane { + private RightSimPaneController controller; + + public RightSimPanePresentation() { + controller = new EcdarFXMLLoader().loadAndGetController("RightSimPanePresentation.fxml", this); + + initializeBackground(); + initializeLeftBorder(); + } + + /** + * Sets the background color of the ScrollPane Vbox + */ + private void initializeBackground() { + controller.scrollPaneVbox.setBackground(new Background(new BackgroundFill( + Color.GREY.getColor(Color.Intensity.I200), + CornerRadii.EMPTY, + Insets.EMPTY + ))); + } + + /** + * Initializes the thin border on the left side of the querypane toolbar + */ + private void initializeLeftBorder() { + setBorder(new Border(new BorderStroke( + Color.GREY_BLUE.getColor(Color.Intensity.I900), + BorderStrokeStyle.SOLID, + CornerRadii.EMPTY, + new BorderWidths(0, 0, 0, 1) + ))); + } + +} diff --git a/src/main/java/ecdar/presentations/SimEdgePresentation.java b/src/main/java/ecdar/presentations/SimEdgePresentation.java new file mode 100755 index 00000000..540a7db0 --- /dev/null +++ b/src/main/java/ecdar/presentations/SimEdgePresentation.java @@ -0,0 +1,50 @@ +package ecdar.presentations; + +import ecdar.Ecdar; +import ecdar.abstractions.Component; +import ecdar.abstractions.Edge; +import ecdar.controllers.SimEdgeController; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.Group; +import javafx.util.Pair; + +/** + * The presentation class for the edges shown in the {@link SimulatorOverviewPresentation} + */ +public class SimEdgePresentation extends Group { + private final SimEdgeController controller; + + private final ObjectProperty edge = new SimpleObjectProperty<>(); + private final ObjectProperty component = new SimpleObjectProperty<>(); + + public SimEdgePresentation(final Edge edge, final Component component) { + controller = new EcdarFXMLLoader().loadAndGetController("SimEdgePresentation.fxml", this); + + controller.setEdge(edge); + this.edge.bind(controller.edgeProperty()); + + controller.setComponent(component); + this.component.bind(controller.componentProperty()); + + // when hovering mouse the curser should change to hand + this.setOnMouseEntered(event -> { + if (Ecdar.getSimulationHandler().currentState.get().getEdges().contains(new Pair<>(component.getName(), edge.getId()))) + this.getScene().setCursor(javafx.scene.Cursor.HAND); + }); + this.setOnMouseExited(event -> this.getScene().setCursor(javafx.scene.Cursor.DEFAULT)); + + // when clicking the edge the edge should be selected and the simulation should take next step (if the edge is enabled) + this.setOnMouseClicked(event -> { + if (Ecdar.getSimulationHandler().currentState.get().getEdges().contains(new Pair<>(component.getName(), edge.getId()))) { + Ecdar.getSimulationHandler().selectedEdge.set(edge); + Ecdar.getSimulationHandler().nextStep(); + } + }); + } + + public SimEdgeController getController() { + return controller; + } + +} diff --git a/src/main/java/ecdar/presentations/SimLocationPresentation.java b/src/main/java/ecdar/presentations/SimLocationPresentation.java new file mode 100755 index 00000000..082a9bb3 --- /dev/null +++ b/src/main/java/ecdar/presentations/SimLocationPresentation.java @@ -0,0 +1,505 @@ +package ecdar.presentations; + +import ecdar.abstractions.Component; +import ecdar.abstractions.Location; +import ecdar.controllers.SimLocationController; +import ecdar.utility.Highlightable; +import ecdar.utility.colors.Color; +import ecdar.utility.helpers.BindingHelper; +import ecdar.utility.helpers.SelectHelper; +import javafx.animation.*; +import javafx.beans.binding.DoubleBinding; +import javafx.beans.property.*; +import javafx.scene.Group; +import javafx.scene.control.Label; +import javafx.scene.effect.DropShadow; +import javafx.scene.paint.Paint; +import javafx.scene.shape.*; +import javafx.util.Duration; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import static ecdar.presentations.LocationPresentation.INITIAL_RADIUS; +import static ecdar.presentations.LocationPresentation.RADIUS; + +/** + * Presentation for a location in the {@link SimulatorOverviewPresentation}. + * This class should be refactored such that the shared code between this class + * and {@link LocationPresentation} is placed in a base class. + */ +public class SimLocationPresentation extends Group implements Highlightable { + + private static int id = 0; + private final SimLocationController controller; + private final Timeline initialAnimation = new Timeline(); + private final Timeline hoverAnimationEntered = new Timeline(); + private final Timeline hoverAnimationExited = new Timeline(); + private final Timeline hiddenAreaAnimationEntered = new Timeline(); + private final Timeline hiddenAreaAnimationExited = new Timeline(); + private final Timeline scaleShakeIndicatorBackgroundAnimation = new Timeline(); + private final Timeline shakeContentAnimation = new Timeline(); + private final List> updateColorDelegates = new ArrayList<>(); + private final DoubleProperty animation = new SimpleDoubleProperty(0); + private final DoubleBinding reverseAnimation = new SimpleDoubleProperty(1).subtract(animation); + private BooleanProperty isPlaced = new SimpleBooleanProperty(true); + + /** + * Constructs a Simulator Location ready to be placed on the view + * @param location the location model, which the presenter should show + * @param component the component where the location is + */ + public SimLocationPresentation(final Location location, final Component component) { + controller = new EcdarFXMLLoader().loadAndGetController("SimLocationPresentation.fxml", this); + + // Bind the component with the one of the controller + controller.setComponent(component); + + // Bind the location with the one of the controller + controller.setLocation(location); + + initializeIdLabel(); + initializeTypeGraphics(); + initializeLocationShapes(); + initializeTags(); + initializeShakeAnimation(); + initializeCircle(); + } + + /** + * Initializes the label in the middle of the location + */ + private void initializeIdLabel() { + final Location location = controller.getLocation(); + final Label idLabel = controller.idLabel; + + final DropShadow ds = new DropShadow(); + ds.setRadius(2); + ds.setSpread(1); + + idLabel.setEffect(ds); + + idLabel.textProperty().bind((location.idProperty())); + + // Center align the label + idLabel.widthProperty().addListener((obsWidth, oldWidth, newWidth) -> idLabel.translateXProperty().set(newWidth.doubleValue() / -2)); + idLabel.heightProperty().addListener((obsHeight, oldHeight, newHeight) -> idLabel.translateYProperty().set(newHeight.doubleValue() / -2)); + + final ObjectProperty color = location.colorProperty(); + final ObjectProperty colorIntensity = location.colorIntensityProperty(); + + // Delegate to style the label based on the color of the location + final BiConsumer updateColor = (newColor, newIntensity) -> { + idLabel.setTextFill(newColor.getTextColor(newIntensity)); + ds.setColor(newColor.getColor(newIntensity)); + }; + + updateColorDelegates.add(updateColor); + + // Set the initial color + updateColor.accept(color.get(), colorIntensity.get()); + + // Update the color of the circle when the color of the location is updated + color.addListener((obs, old, newColor) -> updateColor.accept(newColor, colorIntensity.get())); + } + + /** + * Initialize the tags placed on this location + */ + private void initializeTags() { + + // Set the layout from the model (if they are not both 0) + final Location loc = controller.getLocation(); + if((loc.getNicknameX() != 0) && (loc.getNicknameY() != 0)) { + controller.nicknameTag.setTranslateX(loc.getNicknameX()); + controller.nicknameTag.setTranslateY(loc.getNicknameY()); + } + + if((loc.getInvariantX() != 0) && (loc.getInvariantY() != 0)) { + controller.invariantTag.setTranslateX(loc.getInvariantX()); + controller.invariantTag.setTranslateY(loc.getInvariantY()); + } + + // Bind the model to the layout + loc.nicknameXProperty().bind(controller.nicknameTag.translateXProperty()); + loc.nicknameYProperty().bind(controller.nicknameTag.translateYProperty()); + loc.invariantXProperty().bind(controller.invariantTag.translateXProperty()); + loc.invariantYProperty().bind(controller.invariantTag.translateYProperty()); + + final Consumer updateTags = location -> { + // Update the color + controller.nicknameTag.bindToColor(location.colorProperty(), location.colorIntensityProperty(), true); + controller.invariantTag.bindToColor(location.colorProperty(), location.colorIntensityProperty(), false); + + // Update the invariant + controller.nicknameTag.setAndBindString(location.nicknameProperty()); + controller.invariantTag.setAndBindString(location.invariantProperty()); + + // Set the visibility of the name tag depending on the nickname + final Consumer updateVisibilityFromNickName = (nickname) -> { + if (nickname.equals("")) { + controller.nicknameTag.setOpacity(0); + } else { + controller.nicknameTag.setOpacity(1); + } + }; + + location.nicknameProperty().addListener((obs, oldNickname, newNickname) -> updateVisibilityFromNickName.accept(newNickname)); + updateVisibilityFromNickName.accept(location.getNickname()); + + // Set the visibility of the invariant tag depending on the invariant + final Consumer updateVisibilityFromInvariant = (invariant) -> { + if (invariant.equals("") ) { + controller.invariantTag.setOpacity(0); + } else { + controller.invariantTag.setOpacity(1); + } + }; + + location.invariantProperty().addListener((obs, oldInvariant, newInvariant) -> updateVisibilityFromInvariant.accept(newInvariant)); + updateVisibilityFromInvariant.accept(location.getInvariant()); + + controller.nicknameTag.setComponent(controller.getComponent()); + controller.nicknameTag.setLocationAware(location); + BindingHelper.bind(controller.nameTagLine, controller.nicknameTag); + + controller.invariantTag.setComponent(controller.getComponent()); + controller.invariantTag.setLocationAware(location); + BindingHelper.bind(controller.invariantTagLine, controller.invariantTag); + }; + + // Update the tags when the loc updates + controller.locationProperty().addListener(observable -> updateTags.accept(loc)); + + // Initialize the tags from the current loc + updateTags.accept(loc); + } + + /** + * Initialize the circle which makes up most of the location + */ + private void initializeCircle() { + final Location location = controller.getLocation(); + + final Circle circle = controller.circle; + circle.setRadius(RADIUS); + final ObjectProperty color = location.colorProperty(); + final ObjectProperty colorIntensity = location.colorIntensityProperty(); + + // Delegate to style the label based on the color of the location + final BiConsumer updateColor = (newColor, newIntensity) -> { + circle.setFill(newColor.getColor(newIntensity)); + circle.setStroke(newColor.getColor(newIntensity.next(2))); + }; + + updateColorDelegates.add(updateColor); + + // Set the initial color + updateColor.accept(color.get(), colorIntensity.get()); + + // Update the color of the circle when the color of the location is updated + color.addListener((obs, old, newColor) -> updateColor.accept(newColor, colorIntensity.get())); + colorIntensity.addListener((obs, old, newIntensity) -> updateColor.accept(color.get(), newIntensity)); + } + + /** + * Initializes the shapes of a urgent location + */ + private void initializeLocationShapes() { + final Path notCommittedShape = controller.notCommittedShape; + + // Bind sizes for shape that transforms between urgent and normal + initializeLocationShapes(notCommittedShape, RADIUS); + + final Location location = controller.getLocation(); + + final BiConsumer updateUrgencies = (oldUrgency, newUrgency) -> { + final Transition toUrgent = new Transition() { + { + setCycleDuration(Duration.millis(200)); + } + + @Override + protected void interpolate(final double frac) { + animation.set(frac); + } + }; + + final Transition toNormal = new Transition() { + { + setCycleDuration(Duration.millis(200)); + } + + @Override + protected void interpolate(final double frac) { + animation.set(1-frac); + } + }; + + if(oldUrgency.equals(Location.Urgency.NORMAL) && !newUrgency.equals(Location.Urgency.NORMAL)) { + toUrgent.play(); + } else if(newUrgency.equals(Location.Urgency.NORMAL)) { + toNormal.play(); + } + notCommittedShape.setVisible(true); + }; + + location.urgencyProperty().addListener((obsUrgency, oldUrgency, newUrgency) -> { + updateUrgencies.accept(oldUrgency, newUrgency); + }); + + updateUrgencies.accept(Location.Urgency.NORMAL, location.getUrgency()); + + // Update the colors + final ObjectProperty color = location.colorProperty(); + final ObjectProperty colorIntensity = location.colorIntensityProperty(); + + // Delegate to style the label based on the color of the location + final BiConsumer updateColor = (newColor, newIntensity) -> { + notCommittedShape.setFill(newColor.getColor(newIntensity)); + notCommittedShape.setStroke(newColor.getColor(newIntensity.next(2))); + }; + + updateColorDelegates.add(updateColor); + + // Set the initial color + updateColor.accept(color.get(), colorIntensity.get()); + + // Update the color of the circle when the color of the location is updated + color.addListener((obs, old, newColor) -> updateColor.accept(newColor, colorIntensity.get())); + } + + /** + * Returns true if the mouse hovers over a location + * */ + public Boolean isPlaced(){ + return isPlaced.get(); + } + + /** + * Set placed to true or false + * */ + public void setPlaced(boolean placed){ + isPlaced.set(placed); + } + + private void initializeTypeGraphics() { + final Location location = controller.getLocation(); + + final Path notCommittedInitialIndicator = controller.notCommittedInitialIndicator; + + // Bind visibility and size of normal shape + initializeLocationShapes(notCommittedInitialIndicator, INITIAL_RADIUS); + notCommittedInitialIndicator.visibleProperty().bind(location.typeProperty().isEqualTo(Location.Type.INITIAL)); + // As the style sometimes "forget" its values + notCommittedInitialIndicator.setStrokeType(StrokeType.INSIDE); + notCommittedInitialIndicator.setStroke(Paint.valueOf("white")); + notCommittedInitialIndicator.setFill(Paint.valueOf("transparent")); + } + + public void animateIn() { + initialAnimation.play(); + } + + public void animateHoverEntered() { + + if (shakeContentAnimation.getStatus().equals(Animation.Status.RUNNING)) return; + + hoverAnimationEntered.play(); + } + + public void animateHoverExited() { + if (shakeContentAnimation.getStatus().equals(Animation.Status.RUNNING)) return; + + hoverAnimationExited.play(); + } + + public void animateLocationEntered() { + hiddenAreaAnimationExited.stop(); + hiddenAreaAnimationEntered.play(); + } + + public void animateLocationExited() { + hiddenAreaAnimationEntered.stop(); + hiddenAreaAnimationExited.play(); + } + + + /** + * Initializes the animation of shaking the location. Can for instance be used when the user tries an + * action which is not allowed, i.e. deleting + */ + private void initializeShakeAnimation() { + final Interpolator interpolator = Interpolator.SPLINE(0.645, 0.045, 0.355, 1); + + final KeyValue scale0x = new KeyValue(controller.scaleContent.scaleXProperty(), 1, interpolator); + final KeyValue radius0 = new KeyValue(controller.circleShakeIndicator.radiusProperty(), 0, interpolator); + final KeyValue opacity0 = new KeyValue(controller.circleShakeIndicator.opacityProperty(), 0, interpolator); + + final KeyValue scale1x = new KeyValue(controller.scaleContent.scaleXProperty(), 1.3, interpolator); + final KeyValue radius1 = new KeyValue(controller.circleShakeIndicator.radiusProperty(), controller.circle.getRadius() * 0.85, interpolator); + final KeyValue opacity1 = new KeyValue(controller.circleShakeIndicator.opacityProperty(), 0.2, interpolator); + + final KeyFrame kf1 = new KeyFrame(Duration.millis(0), scale0x, radius0, opacity0); + final KeyFrame kf2 = new KeyFrame(Duration.millis(2500), scale1x, radius1, opacity1); + final KeyFrame kf3 = new KeyFrame(Duration.millis(3300), radius0, opacity0); + final KeyFrame kf4 = new KeyFrame(Duration.millis(3500), scale0x); + final KeyFrame kfEnd = new KeyFrame(Duration.millis(8000)); + + scaleShakeIndicatorBackgroundAnimation.getKeyFrames().addAll(kf1, kf2, kf3, kf4, kfEnd); + + final KeyValue noShakeX = new KeyValue(controller.shakeContent.translateXProperty(), 0, interpolator); + final KeyValue shakeLeftX = new KeyValue(controller.shakeContent.translateXProperty(), -1, interpolator); + final KeyValue shakeRightX = new KeyValue(controller.shakeContent.translateXProperty(), 1, interpolator); + + final KeyFrame[] shakeFrames = { + new KeyFrame(Duration.millis(0), noShakeX), + new KeyFrame(Duration.millis(1450), noShakeX), + + new KeyFrame(Duration.millis(1500), shakeLeftX), + new KeyFrame(Duration.millis(1550), shakeRightX), + new KeyFrame(Duration.millis(1600), shakeLeftX), + new KeyFrame(Duration.millis(1650), shakeRightX), + new KeyFrame(Duration.millis(1700), shakeLeftX), + new KeyFrame(Duration.millis(1750), shakeRightX), + new KeyFrame(Duration.millis(1800), shakeLeftX), + new KeyFrame(Duration.millis(1850), shakeRightX), + new KeyFrame(Duration.millis(1900), shakeLeftX), + new KeyFrame(Duration.millis(1950), shakeRightX), + new KeyFrame(Duration.millis(2000), shakeLeftX), + new KeyFrame(Duration.millis(2050), shakeRightX), + new KeyFrame(Duration.millis(2100), shakeLeftX), + new KeyFrame(Duration.millis(2150), shakeRightX), + new KeyFrame(Duration.millis(2200), shakeLeftX), + new KeyFrame(Duration.millis(2250), shakeRightX), + + new KeyFrame(Duration.millis(2300), noShakeX), + new KeyFrame(Duration.millis(8000)) + }; + + shakeContentAnimation.getKeyFrames().addAll(shakeFrames); + + shakeContentAnimation.setCycleCount(1000); + scaleShakeIndicatorBackgroundAnimation.setCycleCount(1000); + } + + /** + * Plays the shake animation + * @param start false - the animation resets to the beginning
+ * true - the animation starts + */ + public void animateShakeWarning(final boolean start) { + if (start) { + scaleShakeIndicatorBackgroundAnimation.play(); + shakeContentAnimation.play(); + } else { + controller.scaleContent.scaleXProperty().set(1); + scaleShakeIndicatorBackgroundAnimation.playFromStart(); + scaleShakeIndicatorBackgroundAnimation.stop(); + + controller.circleShakeIndicator.setOpacity(0); + shakeContentAnimation.playFromStart(); + shakeContentAnimation.stop(); + } + } + + /** + * Get the controller associated with this presenter + * @return the controller + */ + public SimLocationController getController() { + return controller; + } + + private void initializeLocationShapes(final Path locationShape, final double radius) { + final double c = 0.551915024494; + final double circleToOctagonLineRatio = 0.35; + + final MoveTo moveTo = new MoveTo(); + moveTo.xProperty().bind(animation.multiply(circleToOctagonLineRatio * radius)); + moveTo.yProperty().set(radius); + + final CubicCurveTo cc1 = new CubicCurveTo(); + cc1.controlX1Property().bind(reverseAnimation.multiply(c * radius).add(animation.multiply(circleToOctagonLineRatio * radius))); + cc1.controlY1Property().bind(reverseAnimation.multiply(radius).add(animation.multiply(radius))); + cc1.controlX2Property().bind(reverseAnimation.multiply(radius).add(animation.multiply(radius))); + cc1.controlY2Property().bind(reverseAnimation.multiply(c * radius).add(animation.multiply(circleToOctagonLineRatio * radius))); + cc1.setX(radius); + cc1.yProperty().bind(animation.multiply(circleToOctagonLineRatio * radius)); + + + final LineTo lineTo1 = new LineTo(); + lineTo1.xProperty().bind(cc1.xProperty()); + lineTo1.yProperty().bind(cc1.yProperty().multiply(-1)); + + final CubicCurveTo cc2 = new CubicCurveTo(); + cc2.controlX1Property().bind(cc1.controlX2Property()); + cc2.controlY1Property().bind(cc1.controlY2Property().multiply(-1)); + cc2.controlX2Property().bind(cc1.controlX1Property()); + cc2.controlY2Property().bind(cc1.controlY1Property().multiply(-1)); + cc2.xProperty().bind(moveTo.xProperty()); + cc2.yProperty().bind(moveTo.yProperty().multiply(-1)); + + + final LineTo lineTo2 = new LineTo(); + lineTo2.xProperty().bind(cc2.xProperty().multiply(-1)); + lineTo2.yProperty().bind(cc2.yProperty()); + + final CubicCurveTo cc3 = new CubicCurveTo(); + cc3.controlX1Property().bind(cc2.controlX2Property().multiply(-1)); + cc3.controlY1Property().bind(cc2.controlY2Property()); + cc3.controlX2Property().bind(cc2.controlX1Property().multiply(-1)); + cc3.controlY2Property().bind(cc2.controlY1Property()); + cc3.xProperty().bind(lineTo1.xProperty().multiply(-1)); + cc3.yProperty().bind(lineTo1.yProperty()); + + + final LineTo lineTo3 = new LineTo(); + lineTo3.xProperty().bind(cc3.xProperty()); + lineTo3.yProperty().bind(cc3.yProperty().multiply(-1)); + + final CubicCurveTo cc4 = new CubicCurveTo(); + cc4.controlX1Property().bind(cc3.controlX2Property()); + cc4.controlY1Property().bind(cc3.controlY2Property().multiply(-1)); + cc4.controlX2Property().bind(cc3.controlX1Property()); + cc4.controlY2Property().bind(cc3.controlY1Property().multiply(-1)); + cc4.xProperty().bind(lineTo2.xProperty()); + cc4.yProperty().bind(lineTo2.yProperty().multiply(-1)); + + + final LineTo lineTo4 = new LineTo(); + lineTo4.xProperty().bind(moveTo.xProperty()); + lineTo4.yProperty().bind(moveTo.yProperty()); + + + locationShape.getElements().add(moveTo); + locationShape.getElements().add(cc1); + + locationShape.getElements().add(lineTo1); + locationShape.getElements().add(cc2); + + locationShape.getElements().add(lineTo2); + locationShape.getElements().add(cc3); + + locationShape.getElements().add(lineTo3); + locationShape.getElements().add(cc4); + + locationShape.getElements().add(lineTo4); + } + + @Override + public void highlight() { + updateColorDelegates.forEach(colorConsumer -> colorConsumer.accept(SelectHelper.SELECT_COLOR, SelectHelper.SELECT_COLOR_INTENSITY_NORMAL)); + } + + @Override + public void unhighlight() { + updateColorDelegates.forEach(colorConsumer -> { + final Location location = controller.getLocation(); + + colorConsumer.accept(location.getColor(), location.getColorIntensity()); + }); + } +} \ No newline at end of file diff --git a/src/main/java/ecdar/presentations/SimNailPresentation.java b/src/main/java/ecdar/presentations/SimNailPresentation.java new file mode 100755 index 00000000..250f2f9d --- /dev/null +++ b/src/main/java/ecdar/presentations/SimNailPresentation.java @@ -0,0 +1,258 @@ +package ecdar.presentations; + +import ecdar.abstractions.Component; +import ecdar.abstractions.Edge; +import ecdar.abstractions.EdgeStatus; +import ecdar.abstractions.Nail; +import ecdar.controllers.SimEdgeController; +import ecdar.controllers.SimNailController; +import ecdar.utility.Highlightable; +import ecdar.utility.colors.Color; +import ecdar.utility.helpers.BindingHelper; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.scene.Group; +import javafx.scene.control.Label; +import javafx.scene.shape.Line; + +import java.util.function.Consumer; + +import static javafx.util.Duration.millis; + +/** + * The presentation for the nail shown on a {@link SimEdgePresentation} in the {@link SimulatorOverviewPresentation}
+ * This class should be refactored such that code which are duplicated from {@link NailPresentation} + * have its own base class. + */ +public class SimNailPresentation extends Group implements Highlightable { + + public static final double COLLAPSED_RADIUS = 2d; + public static final double HOVERED_RADIUS = 7d; + + private final SimNailController controller; + private final Timeline shakeAnimation = new Timeline(); + + /** + * Constructs a nail which is ready to be displayed in the simulator + * @param nail the nail which should be presented + * @param edge the edge where the nail belongs to + * @param component the component where the nail belongs + * @param simEdgeController the controller of the edge where the nail belongs to + */ + public SimNailPresentation(final Nail nail, final Edge edge, final Component component, final SimEdgeController simEdgeController) { + controller = new EcdarFXMLLoader().loadAndGetController("SimNailPresentation.fxml", this); + + // Bind the component with the one of the controller + controller.setComponent(component); + + // Bind the edge with the one of the controller + controller.setEdge(edge); + // Bind the nail with the one of the controller + controller.setNail(nail); + + controller.setEdgeController(simEdgeController); + + initializeNailCircleColor(); + initializePropertyTag(); + initializeRadius(); + initializeShakeAnimation(); + } + + /** + * Sets the radius of the nail + */ + private void initializeRadius() { + final Consumer radiusUpdater = (propertyType) -> { + if(!propertyType.equals(Edge.PropertyType.NONE)) { + controller.getNail().setRadius(SimNailPresentation.HOVERED_RADIUS); + } + }; + controller.getNail().propertyTypeProperty().addListener((observable, oldValue, newValue) -> { + radiusUpdater.accept(newValue); + }); + radiusUpdater.accept(controller.getNail().getPropertyType()); + } + + /** + * Initializes the text / PropertyTag shown on a {@link SimTagPresentation} on the nail. + */ + private void initializePropertyTag() { + final SimTagPresentation propertyTag = controller.propertyTag; + final Line propertyTagLine = controller.propertyTagLine; + propertyTag.setComponent(controller.getComponent()); + propertyTag.setLocationAware(controller.getNail()); + + // Bind the line to the tag + BindingHelper.bind(propertyTagLine, propertyTag); + + // Bind the color of the tag to the color of the component + propertyTag.bindToColor(controller.getComponent().colorProperty(), controller.getComponent().colorIntensityProperty()); + + // Updates visibility and placeholder of the tag depending on the type of nail + final Consumer updatePropertyType = (propertyType) -> { + + // If it is not a property nail hide the tag otherwise show it and write proper placeholder + if(propertyType.equals(Edge.PropertyType.NONE)) { + propertyTag.setVisible(false); + } else { + + // Show the property tag since the nail is a property nail + propertyTag.setVisible(true); + + // Set and bind the location of the property tag + if((controller.getNail().getPropertyX() != 0) && (controller.getNail().getPropertyY() != 0)) { + propertyTag.setTranslateX(controller.getNail().getPropertyX()); + propertyTag.setTranslateY(controller.getNail().getPropertyY()); + } + controller.getNail().propertyXProperty().bind(propertyTag.translateXProperty()); + controller.getNail().propertyYProperty().bind(propertyTag.translateYProperty()); + + final Label propertyLabel = controller.propertyLabel; + + if(propertyType.equals(Edge.PropertyType.SELECTION)) { + propertyLabel.setText(":"); + propertyLabel.setTranslateX(-3); + propertyLabel.setTranslateY(-8); + propertyTag.setAndBindString(controller.getEdge().selectProperty()); + } else if(propertyType.equals(Edge.PropertyType.GUARD)) { + propertyLabel.setText("<"); + propertyLabel.setTranslateX(-3); + propertyLabel.setTranslateY(-7); + propertyTag.setAndBindString(controller.getEdge().guardProperty()); + } else if(propertyType.equals(Edge.PropertyType.SYNCHRONIZATION)) { + updateSyncLabel(); + propertyLabel.setTranslateX(-3); + propertyLabel.setTranslateY(-7); + propertyTag.setAndBindString(controller.getEdge().syncProperty()); + } else if(propertyType.equals(Edge.PropertyType.UPDATE)) { + propertyLabel.setText("="); + propertyLabel.setTranslateX(-3); + propertyLabel.setTranslateY(-7); + propertyTag.setAndBindString(controller.getEdge().updateProperty()); + } + + //Disable the ability to edit the tag if the nails edge is locked + if(controller.getEdge().getIsLockedProperty().getValue()){ + propertyTag.setDisabledText(true); + } + } + }; + + // Whenever the property type updates, update the tag + controller.getNail().propertyTypeProperty().addListener((obs, oldPropertyType, newPropertyType) -> { + updatePropertyType.accept(newPropertyType); + }); + + // Whenever the edge changes I/O status, if sync nail then update its label + controller.getEdge().ioStatus.addListener((observable, oldValue, newValue) -> { + if (controller.getNail().getPropertyType().equals(Edge.PropertyType.SYNCHRONIZATION)) + updateSyncLabel(); + }); + + // Update the tag initially + updatePropertyType.accept(controller.getNail().getPropertyType()); + } + + /** + * Updates the synchronization label and tag. + * The update depends on the edge I/O status. + */ + private void updateSyncLabel() { + final Label propertyLabel = controller.propertyLabel; + + // show ? or ! dependent on edge I/O status + if (controller.getEdge().ioStatus.get().equals(EdgeStatus.INPUT)) { + propertyLabel.setText("?"); + } else { + propertyLabel.setText("!"); + } + } + + /** + * Set up Listeners for updating the color of the nail, set the color initially + */ + private void initializeNailCircleColor() { + final Runnable updateNailColor = () -> { + final Color color = controller.getComponent().getColor(); + final Color.Intensity colorIntensity = controller.getComponent().getColorIntensity(); + + if(!controller.getNail().getPropertyType().equals(Edge.PropertyType.NONE)) { + controller.nailCircle.setFill(color.getColor(colorIntensity)); + controller.nailCircle.setStroke(color.getColor(colorIntensity.next(2))); + } else { + controller.nailCircle.setFill(Color.GREY_BLUE.getColor(Color.Intensity.I800)); + controller.nailCircle.setStroke(Color.GREY_BLUE.getColor(Color.Intensity.I900)); + } + }; + + // When the color of the component updates, update the nail indicator as well + controller.getComponent().colorProperty().addListener((observable) -> updateNailColor.run()); + + // When the color intensity of the component updates, update the nail indicator + controller.getComponent().colorIntensityProperty().addListener((observable) -> updateNailColor.run()); + + // Initialize the color of the nail + updateNailColor.run(); + } + + /** + * Initializes a shake animation found in {@link SimNailPresentation#shakeAnimation} which can + * be played using {@link SimNailPresentation#shake()} + */ + private void initializeShakeAnimation() { + final Interpolator interpolator = Interpolator.SPLINE(0.645, 0.045, 0.355, 1); + + final double startX = controller.root.getTranslateX(); + final KeyValue kv1 = new KeyValue(controller.root.translateXProperty(), startX - 3, interpolator); + final KeyValue kv2 = new KeyValue(controller.root.translateXProperty(), startX + 3, interpolator); + final KeyValue kv3 = new KeyValue(controller.root.translateXProperty(), startX, interpolator); + + final KeyFrame kf1 = new KeyFrame(millis(50), kv1); + final KeyFrame kf2 = new KeyFrame(millis(100), kv2); + final KeyFrame kf3 = new KeyFrame(millis(150), kv1); + final KeyFrame kf4 = new KeyFrame(millis(200), kv2); + final KeyFrame kf5 = new KeyFrame(millis(250), kv3); + + shakeAnimation.getKeyFrames().addAll(kf1, kf2, kf3, kf4, kf5); + } + + /** + * Plays the {@link SimNailPresentation#shakeAnimation} + */ + public void shake() { + shakeAnimation.play(); + } + + /** + * Highlights the nail + */ + @Override + public void highlight() { + final Color color = Color.DEEP_ORANGE; + final Color.Intensity intensity = Color.Intensity.I500; + + // Set the color + controller.nailCircle.setFill(color.getColor(intensity)); + controller.nailCircle.setStroke(color.getColor(intensity.next(2))); + } + + /** + * Removes the highlight from the nail + */ + @Override + public void unhighlight() { + Color color = Color.GREY_BLUE; + Color.Intensity intensity = Color.Intensity.I800; + + // Set the color + if(!controller.getNail().getPropertyType().equals(Edge.PropertyType.NONE)) { + color = controller.getComponent().getColor(); + intensity = controller.getComponent().getColorIntensity(); + } + + controller.nailCircle.setFill(color.getColor(intensity)); + controller.nailCircle.setStroke(color.getColor(intensity.next(2))); + } +} diff --git a/src/main/java/ecdar/presentations/SimTagPresentation.java b/src/main/java/ecdar/presentations/SimTagPresentation.java new file mode 100755 index 00000000..d2916ee1 --- /dev/null +++ b/src/main/java/ecdar/presentations/SimTagPresentation.java @@ -0,0 +1,186 @@ +package ecdar.presentations; + +import ecdar.abstractions.Component; +import ecdar.utility.colors.Color; +import ecdar.utility.helpers.LocationAware; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.StringProperty; +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.layout.StackPane; +import javafx.scene.shape.LineTo; +import javafx.scene.shape.MoveTo; +import javafx.scene.shape.Path; + +import java.util.function.BiConsumer; + +import static ecdar.presentations.Grid.GRID_SIZE; + +/** + * The presentation for the tag shown on a {@link SimEdgePresentation} in the {@link SimulatorOverviewPresentation}
+ * This class should be refactored such that code which are duplicated from {@link TagPresentation} + * have its own base class. + */ +public class SimTagPresentation extends StackPane { + + private final static Color backgroundColor = Color.GREY; + private final static Color.Intensity backgroundColorIntensity = Color.Intensity.I50; + + private final ObjectProperty component = new SimpleObjectProperty<>(null); + private final ObjectProperty locationAware = new SimpleObjectProperty<>(null); + + private LineTo l2; + private LineTo l3; + + private static double TAG_HEIGHT = 1.6 * GRID_SIZE; + + /** + * Constructs the {@link SimTagPresentation} + */ + public SimTagPresentation() { + new EcdarFXMLLoader().loadAndGetController("SimTagPresentation.fxml", this); + initializeShape(); + initializeLabel(); + initializeMouseTransparency(); + } + + private void initializeMouseTransparency() { + mouseTransparentProperty().bind(opacityProperty().isEqualTo(0, 0.00f)); + } + + /** + * Initializes the label which shows the property + */ + private void initializeLabel() { + final Label label = (Label) lookup("#label"); + final Path shape = (Path) lookup("#shape"); + + final Insets insets = new Insets(0,2,0,2); + label.setPadding(insets); + + final int padding = 0; + + label.layoutBoundsProperty().addListener((obs, oldBounds, newBounds) -> { + double newWidth = Math.max(newBounds.getWidth(), 10); + final double res = GRID_SIZE * 2 - (newWidth % (GRID_SIZE * 2)); + newWidth += res; + + l2.setX(newWidth + padding); + l3.setX(newWidth + padding); + + setMinWidth(newWidth + padding); + setMaxWidth(newWidth + padding); + + label.focusedProperty().addListener((observable, oldFocused, newFocused) -> { + if (newFocused) { + shape.setTranslateY(2); + label.setTranslateY(2); + } + }); + + if (getWidth() >= 1000) { + setWidth(newWidth); + setHeight(TAG_HEIGHT); + shape.setTranslateY(-1); + } + + // Fixes the jumping of the shape when the text field is empty + if (label.getText().isEmpty()) { + shape.setLayoutX(0); + } + }); + } + + /** + * Initialize the shape which is around the property label + */ + private void initializeShape() { + final int WIDTH = 5000; + final double HEIGHT = TAG_HEIGHT; + + final Path shape = (Path) lookup("#shape"); + + final MoveTo start = new MoveTo(0, 0); + + l2 = new LineTo(WIDTH, 0); + l3 = new LineTo(WIDTH, HEIGHT); + final LineTo l4 = new LineTo(0, HEIGHT); + final LineTo l6 = new LineTo(0, 0); + + shape.getElements().addAll(start, l2, l3, l4, l6); + shape.setFill(backgroundColor.getColor(backgroundColorIntensity)); + shape.setStroke(backgroundColor.getColor(backgroundColorIntensity.next(4))); + } + + public void bindToColor(final ObjectProperty color, final ObjectProperty intensity) { + bindToColor(color, intensity, false); + } + + public void bindToColor(final ObjectProperty color, final ObjectProperty intensity, final boolean doColorBackground) { + final BiConsumer recolor = (newColor, newIntensity) -> { + if (doColorBackground) { + final Path shape = (Path) lookup("#shape"); + shape.setFill(newColor.getColor(newIntensity.next(-1))); + shape.setStroke(newColor.getColor(newIntensity.next(-1).next(2))); + } + }; + + color.addListener(observable -> recolor.accept(color.get(), intensity.get())); + intensity.addListener(observable -> recolor.accept(color.get(), intensity.get())); + recolor.accept(color.get(), intensity.get()); + } + + /** + * Updates the label with the given string and binds updates from the label on the given {@link StringProperty} + * @param string the string to set and bind to + */ + public void setAndBindString(final StringProperty string) { + final Label label = (Label) lookup("#label"); + + label.textProperty().unbind(); + label.setText(string.get()); + string.bind(label.textProperty()); + } + + /** + * Replaces spaces with underscores in the label + */ +/* public void replaceSpace() { + initializeTextAid(); + }*/ + + public Component getComponent() { + return component.get(); + } + + public void setComponent(final Component component) { + this.component.set(component); + } + + public ObjectProperty componentProperty() { + return component; + } + + public LocationAware getLocationAware() { + return locationAware.get(); + } + + public ObjectProperty locationAwareProperty() { + return locationAware; + } + + public void setLocationAware(LocationAware locationAware) { + this.locationAware.set(locationAware); + } + + /** + * Sets the disabled property in the label + * @param bool true --- the label will be disabled + * false --- the label will be enabled + */ + public void setDisabledText(boolean bool){ + final Label label = (Label) lookup("#label"); + label.setDisable(true); + } +} diff --git a/src/main/java/ecdar/presentations/SimulationInitializationDialogPresentation.java b/src/main/java/ecdar/presentations/SimulationInitializationDialogPresentation.java new file mode 100644 index 00000000..e9c9664a --- /dev/null +++ b/src/main/java/ecdar/presentations/SimulationInitializationDialogPresentation.java @@ -0,0 +1,17 @@ +package ecdar.presentations; + +import com.jfoenix.controls.JFXDialog; +import ecdar.controllers.SimulationInitializationDialogController; + +public class SimulationInitializationDialogPresentation extends JFXDialog { + private final SimulationInitializationDialogController controller; + + public SimulationInitializationDialogPresentation() { + controller = new EcdarFXMLLoader().loadAndGetController("SimulationInitializationDialogPresentation.fxml", this); + } + + public SimulationInitializationDialogController getController() { + return controller; + } + +} diff --git a/src/main/java/ecdar/presentations/SimulatorOverviewPresentation.java b/src/main/java/ecdar/presentations/SimulatorOverviewPresentation.java new file mode 100755 index 00000000..15a34e54 --- /dev/null +++ b/src/main/java/ecdar/presentations/SimulatorOverviewPresentation.java @@ -0,0 +1,20 @@ +package ecdar.presentations; + +import ecdar.controllers.SimulatorOverviewController; +import javafx.scene.layout.AnchorPane; + +/** + * The presenter of the middle part of the simulator. + * It is here where processes of a simulation will be shown. + */ +public class SimulatorOverviewPresentation extends AnchorPane { + private final SimulatorOverviewController controller; + + public SimulatorOverviewPresentation() { + controller = new EcdarFXMLLoader().loadAndGetController("SimulatorOverviewPresentation.fxml", this); + } + + public SimulatorOverviewController getController() { + return controller; + } +} diff --git a/src/main/java/ecdar/presentations/SimulatorPresentation.java b/src/main/java/ecdar/presentations/SimulatorPresentation.java new file mode 100755 index 00000000..255b7dd1 --- /dev/null +++ b/src/main/java/ecdar/presentations/SimulatorPresentation.java @@ -0,0 +1,20 @@ +package ecdar.presentations; + +import ecdar.controllers.SimulatorController; +import javafx.scene.layout.StackPane; + +public class SimulatorPresentation extends StackPane { + private final SimulatorController controller; + + public SimulatorPresentation() { + controller = new EcdarFXMLLoader().loadAndGetController("SimulatorPresentation.fxml", this); + } + + /** + * The way to get the associated/linked controller of this presenter + * @return the controller linked to this presenter + */ + public SimulatorController getController() { + return controller; + } +} diff --git a/src/main/java/ecdar/presentations/TagPresentation.java b/src/main/java/ecdar/presentations/TagPresentation.java index d0075be8..f5b32273 100644 --- a/src/main/java/ecdar/presentations/TagPresentation.java +++ b/src/main/java/ecdar/presentations/TagPresentation.java @@ -8,6 +8,7 @@ import ecdar.utility.helpers.LocationAware; import com.jfoenix.controls.JFXTextField; import ecdar.utility.helpers.SelectHelper; +import ecdar.utility.helpers.StringHelper; import javafx.application.Platform; import javafx.beans.binding.When; import javafx.beans.property.*; @@ -16,10 +17,8 @@ import javafx.geometry.Insets; import javafx.scene.Cursor; import javafx.scene.control.Label; -import javafx.scene.input.KeyCode; import javafx.scene.input.MouseEvent; import javafx.scene.layout.StackPane; -import javafx.scene.robot.Robot; import javafx.scene.shape.LineTo; import javafx.scene.shape.MoveTo; import javafx.scene.shape.Path; @@ -283,6 +282,14 @@ public void replaceSpace() { initializeTextAid((JFXTextField) lookup("#textField")); } + public void replaceSigns() { + var textField = (JFXTextField) lookup("#textField"); + textField.textProperty().addListener((obs, oldText, newText) -> { + textField.setText(StringHelper.ConvertSymbolsToUnicode(newText)); + }); + } + + public void requestTextFieldFocus() { final JFXTextField textField = (JFXTextField) lookup("#textField"); Platform.runLater(textField::requestFocus); diff --git a/src/main/java/ecdar/presentations/TracePaneElementPresentation.java b/src/main/java/ecdar/presentations/TracePaneElementPresentation.java new file mode 100755 index 00000000..0e8be627 --- /dev/null +++ b/src/main/java/ecdar/presentations/TracePaneElementPresentation.java @@ -0,0 +1,96 @@ +package ecdar.presentations; + +import com.jfoenix.controls.JFXRippler; +import ecdar.controllers.TracePaneElementController; +import ecdar.utility.colors.Color; +import javafx.geometry.Insets; +import javafx.scene.Cursor; +import javafx.scene.layout.*; + +import java.util.function.BiConsumer; + +/** + * The presentation class for the trace element that can be inserted into the simulator panes + */ +public class TracePaneElementPresentation extends VBox { + final private TracePaneElementController controller; + + public TracePaneElementPresentation() { + controller = new EcdarFXMLLoader().loadAndGetController("TracePaneElementPresentation.fxml", this); + + initializeToolbar(); + initializeSummaryView(); + } + + /** + * Initializes the tool bar that contains the trace pane element's title and buttons + * Sets the color of the bar and title label. Also sets the look of the rippler effect + */ + private void initializeToolbar() { + final Color color = Color.GREY_BLUE; + final Color.Intensity colorIntensity = Color.Intensity.I800; + + controller.toolbar.setBackground(new Background(new BackgroundFill( + color.getColor(colorIntensity), + CornerRadii.EMPTY, + Insets.EMPTY))); + controller.traceTitle.setTextFill(color.getTextColor(colorIntensity)); + + controller.expandTrace.setMaskType(JFXRippler.RipplerMask.CIRCLE); + controller.expandTrace.setRipplerFill(color.getTextColor(colorIntensity)); + } + + /** + * Initializes the summary view so it is update when steps are taken in the trace. + * Also changes the color and cursor when mouse enters and exits the summary view. + */ + private void initializeSummaryView() { + controller.getNumberOfStepsProperty().addListener( + (observable, oldValue, newValue) -> updateSummaryTitle(newValue.intValue())); + + final Color color = Color.GREY_BLUE; + final Color.Intensity colorIntensity = Color.Intensity.I50; + + final BiConsumer setBackground = (newColor, newIntensity) -> { + controller.traceSummary.setBackground(new Background(new BackgroundFill( + newColor.getColor(newIntensity), + CornerRadii.EMPTY, + Insets.EMPTY + ))); + + controller.traceSummary.setBorder(new Border(new BorderStroke( + newColor.getColor(newIntensity.next(2)), + BorderStrokeStyle.SOLID, + CornerRadii.EMPTY, + new BorderWidths(0, 0, 1, 0) + ))); + }; + + // Update the background when hovered + controller.traceSummary.setOnMouseEntered(event -> { + setBackground.accept(color, colorIntensity.next()); + setCursor(Cursor.HAND); + }); + + // Update the background when the mouse exits + controller.traceSummary.setOnMouseExited(event -> { + setBackground.accept(color, colorIntensity); + setCursor(Cursor.DEFAULT); + }); + + // Update the background initially + setBackground.accept(color, colorIntensity); + } + + /** + * Updates the text of the summary title label with the current number of steps in the trace + * @param steps The number of steps in the trace + */ + private void updateSummaryTitle(int steps) { + controller.summaryTitleLabel.setText(steps + " number of steps in trace"); + } + + public TracePaneElementController getController() { + return controller; + } +} diff --git a/src/main/java/ecdar/presentations/TransitionPaneElementPresentation.java b/src/main/java/ecdar/presentations/TransitionPaneElementPresentation.java new file mode 100755 index 00000000..8453af64 --- /dev/null +++ b/src/main/java/ecdar/presentations/TransitionPaneElementPresentation.java @@ -0,0 +1,69 @@ +package ecdar.presentations; + +import com.jfoenix.controls.JFXRippler; +import ecdar.controllers.TransitionPaneElementController; +import ecdar.utility.colors.Color; +import javafx.geometry.Insets; +import javafx.scene.layout.*; + +/** + * The presentation class for the transition pane element that can be inserted into the simulator panes + */ +public class TransitionPaneElementPresentation extends VBox { + final private TransitionPaneElementController controller; + + public TransitionPaneElementPresentation() { + controller = new EcdarFXMLLoader().loadAndGetController("TransitionPaneElementPresentation.fxml", this); + + initializeToolbar(); + initializeDelayChooser(); + } + + /** + * Initializes the toolbar for the transition pane element. + * Sets the background of the toolbar and changes the title color. + * Also changes the look of the rippler effect. + */ + private void initializeToolbar() { + final Color color = Color.GREY_BLUE; + final Color.Intensity colorIntensity = Color.Intensity.I800; + + // Set the background of the toolbar + controller.toolbar.setBackground(new Background(new BackgroundFill( + color.getColor(colorIntensity), + CornerRadii.EMPTY, + Insets.EMPTY))); + // Set the font color of elements in the toolbar + controller.toolbarTitle.setTextFill(color.getTextColor(colorIntensity)); + + controller.refreshRippler.setMaskType(JFXRippler.RipplerMask.CIRCLE); + controller.refreshRippler.setRipplerFill(color.getTextColor(colorIntensity)); + + controller.expandTransition.setMaskType(JFXRippler.RipplerMask.CIRCLE); + controller.expandTransition.setRipplerFill(color.getTextColor(colorIntensity)); + } + + /** + * Sets the background color of the delay chooser + */ + private void initializeDelayChooser() { + final Color color = Color.GREY_BLUE; + final Color.Intensity colorIntensity = Color.Intensity.I50; + controller.delayChooser.setBackground(new Background(new BackgroundFill( + color.getColor(colorIntensity), + CornerRadii.EMPTY, + Insets.EMPTY + ))); + + controller.delayChooser.setBorder(new Border(new BorderStroke( + color.getColor(colorIntensity.next(2)), + BorderStrokeStyle.SOLID, + CornerRadii.EMPTY, + new BorderWidths(0, 0, 1, 0) + ))); + } + + public TransitionPaneElementController getController() { + return controller; + } +} diff --git a/src/main/java/ecdar/presentations/TransitionPresentation.java b/src/main/java/ecdar/presentations/TransitionPresentation.java new file mode 100755 index 00000000..15fb47e6 --- /dev/null +++ b/src/main/java/ecdar/presentations/TransitionPresentation.java @@ -0,0 +1,98 @@ +package ecdar.presentations; + +import com.jfoenix.controls.JFXRippler; +import ecdar.controllers.TransitionController; +import ecdar.utility.colors.Color; +import javafx.animation.FadeTransition; +import javafx.animation.Interpolator; +import javafx.geometry.Insets; +import javafx.scene.Cursor; +import javafx.scene.layout.*; +import javafx.util.Duration; + +import java.util.function.BiConsumer; + +/** + * The presentation class for a transition view element. + * It represents a single transition and may be used by classes like {@see TransitionPaneElementController} + * to show a list of transitions + */ +public class TransitionPresentation extends AnchorPane { + private TransitionController controller; + private FadeTransition transition; + + public TransitionPresentation() { + controller = new EcdarFXMLLoader().loadAndGetController("TransitionPresentation.fxml", this); + + initializeRippler(); + initializeColors(); + initializeFadeAnimation(); + } + + /** + * Initializes the rippler. + * Sets the color, mask and position of the rippler effect. + */ + private void initializeRippler() { + final Color color = Color.GREY_BLUE; + final Color.Intensity colorIntensity = Color.Intensity.I400; + + controller.rippler.setMaskType(JFXRippler.RipplerMask.RECT); + controller.rippler.setRipplerFill(color.getColor(colorIntensity)); + controller.rippler.setPosition(JFXRippler.RipplerPos.BACK); + } + + /** + * Initializes the colors of the view. + * The background of the view changes colors depending on whether a mouse enters or exits the view. + */ + private void initializeColors() { + final Color color = Color.GREY_BLUE; + final Color.Intensity colorIntensity = Color.Intensity.I50; + + final BiConsumer setBackground = (newColor, newIntensity) -> { + setBackground(new Background(new BackgroundFill( + newColor.getColor(newIntensity), + CornerRadii.EMPTY, + Insets.EMPTY + ))); + + setBorder(new Border(new BorderStroke( + newColor.getColor(newIntensity.next(2)), + BorderStrokeStyle.SOLID, + CornerRadii.EMPTY, + new BorderWidths(0, 0, 1, 0) + ))); + }; + + // Update the background when hovered + setOnMouseEntered(event -> { + setBackground.accept(color, colorIntensity.next()); + setCursor(Cursor.HAND); + }); + + // Update the background when the mouse exits + setOnMouseExited(event -> { + setBackground.accept(color, colorIntensity); + setCursor(Cursor.DEFAULT); + }); + + // Update the background initially + setBackground.accept(color, colorIntensity); + } + + private void initializeFadeAnimation() { + this.transition = new FadeTransition(Duration.millis(500), this); + transition.setFromValue(0); + transition.setToValue(1); + transition.setInterpolator(Interpolator.EASE_IN); + } + + public void playFadeAnimation() { + this.transition.play(); + } + + public TransitionController getController() { + return controller; + } +} diff --git a/src/main/java/ecdar/simulation/SimulationState.java b/src/main/java/ecdar/simulation/SimulationState.java new file mode 100644 index 00000000..a6b4f5e3 --- /dev/null +++ b/src/main/java/ecdar/simulation/SimulationState.java @@ -0,0 +1,54 @@ +package ecdar.simulation; + +import EcdarProtoBuf.ObjectProtos; +import EcdarProtoBuf.ObjectProtos.State; +import ecdar.Ecdar; +import javafx.util.Pair; + +import java.util.ArrayList; + +public class SimulationState { + // locations and edges are saved as key-value pair where key is component name and value = id + private final ArrayList> locations; + private final ArrayList> edges; + private final State state; + + public SimulationState(ObjectProtos.DecisionPoint decisionPoint) { + locations = new ArrayList<>(); + for (ObjectProtos.Location location : decisionPoint.getSource().getLocationTuple().getLocationsList()) { + locations.add(new Pair<>(location.getSpecificComponent().getComponentName(), location.getId())); + } + + edges = new ArrayList<>(); + if (decisionPoint.getEdgesList().isEmpty()) { + Ecdar.showToast("No available transitions."); + } + for (ObjectProtos.Edge edge : decisionPoint.getEdgesList()) { + edges.add(new Pair<>(getComponentName(edge.getId()), edge.getId())); + } + state = decisionPoint.getSource(); + } + + public ArrayList> getLocations() { + return locations; + } + public ArrayList> getEdges() { + return edges; + } + + public State getState() { + return state; + } + + private String getComponentName(String id) { + var components = Ecdar.getProject().getComponents(); + for (var component : components) { + for (var edge : component.getEdges()) { + if (edge.getId().equals(id)) { + return component.getName(); + } + } + } + throw new RuntimeException("Could not find component name for edge with id " + id); + } +} diff --git a/src/main/java/ecdar/simulation/Transition.java b/src/main/java/ecdar/simulation/Transition.java new file mode 100644 index 00000000..da5bac14 --- /dev/null +++ b/src/main/java/ecdar/simulation/Transition.java @@ -0,0 +1,17 @@ +package ecdar.simulation; + +import ecdar.abstractions.Edge; + +import java.util.ArrayList; + +public class Transition { + public String getLabel() { + // ToDo: Implement + return "Transition label"; + } + + public ArrayList getEdges() { + // ToDo: Implement + return new ArrayList<>(); + } +} diff --git a/src/main/java/ecdar/utility/colors/EnabledColor.java b/src/main/java/ecdar/utility/colors/EnabledColor.java index 78cfc10b..8ef4e647 100644 --- a/src/main/java/ecdar/utility/colors/EnabledColor.java +++ b/src/main/java/ecdar/utility/colors/EnabledColor.java @@ -9,13 +9,13 @@ public class EnabledColor { public static final ArrayList enabledColors = new ArrayList() {{ add(new EnabledColor(Color.GREY_BLUE, Color.Intensity.I700, KeyCode.DIGIT0)); add(new EnabledColor(Color.DEEP_ORANGE, Color.Intensity.I700, KeyCode.DIGIT1)); - add(new EnabledColor(Color.RED, Color.Intensity.I500, KeyCode.DIGIT2)); - add(new EnabledColor(Color.PINK, Color.Intensity.I500, KeyCode.DIGIT3)); - add(new EnabledColor(Color.PURPLE, Color.Intensity.I500, KeyCode.DIGIT4)); - add(new EnabledColor(Color.INDIGO, Color.Intensity.I500, KeyCode.DIGIT5)); - add(new EnabledColor(Color.BLUE, Color.Intensity.I600, KeyCode.DIGIT6)); - add(new EnabledColor(Color.CYAN, Color.Intensity.I700, KeyCode.DIGIT7)); - add(new EnabledColor(Color.GREEN, Color.Intensity.I600, KeyCode.DIGIT8)); + add(new EnabledColor(Color.PINK, Color.Intensity.I500, KeyCode.DIGIT2)); + add(new EnabledColor(Color.PURPLE, Color.Intensity.I500, KeyCode.DIGIT3)); + add(new EnabledColor(Color.INDIGO, Color.Intensity.I500, KeyCode.DIGIT4)); + add(new EnabledColor(Color.BLUE, Color.Intensity.I600, KeyCode.DIGIT5)); + add(new EnabledColor(Color.CYAN, Color.Intensity.I700, KeyCode.DIGIT6)); + add(new EnabledColor(Color.GREEN, Color.Intensity.I600, KeyCode.DIGIT7)); + add(new EnabledColor(Color.LIME, Color.Intensity.I500, KeyCode.DIGIT8)); add(new EnabledColor(Color.BROWN, Color.Intensity.I500, KeyCode.DIGIT9)); }}; diff --git a/src/main/java/ecdar/utility/helpers/BindingHelper.java b/src/main/java/ecdar/utility/helpers/BindingHelper.java index afbddec2..89d1a5e6 100644 --- a/src/main/java/ecdar/utility/helpers/BindingHelper.java +++ b/src/main/java/ecdar/utility/helpers/BindingHelper.java @@ -5,6 +5,7 @@ import ecdar.model_canvas.arrow_heads.ChannelReceiverArrowHead; import ecdar.model_canvas.arrow_heads.ChannelSenderArrowHead; import ecdar.presentations.Link; +import ecdar.presentations.SimTagPresentation; import ecdar.presentations.TagPresentation; import ecdar.utility.mouse.MouseTracker; import javafx.beans.binding.DoubleBinding; @@ -24,6 +25,15 @@ public static void bind(final Line subject, final TagPresentation target) { subject.visibleProperty().bind(target.visibleProperty()); } + public static void bind(final Line subject, final SimTagPresentation target) { + subject.startXProperty().set(0); + subject.startYProperty().set(0); + subject.endXProperty().bind(target.translateXProperty().add(target.minWidthProperty().divide(2))); + subject.endYProperty().bind(target.translateYProperty().add(target.heightProperty().divide(2))); + subject.opacityProperty().bind(target.opacityProperty()); + subject.visibleProperty().bind(target.visibleProperty()); + } + public static void bind(final Circular subject, final ObservableDoubleValue x, final ObservableDoubleValue y) { subject.xProperty().bind(EcdarController.getActiveCanvasPresentation().mouseTracker.gridXProperty().subtract(x)); subject.yProperty().bind(EcdarController.getActiveCanvasPresentation().mouseTracker.gridYProperty().subtract(y)); diff --git a/src/main/java/ecdar/utility/helpers/ItemDragHelper.java b/src/main/java/ecdar/utility/helpers/ItemDragHelper.java index b12375c5..2b601271 100644 --- a/src/main/java/ecdar/utility/helpers/ItemDragHelper.java +++ b/src/main/java/ecdar/utility/helpers/ItemDragHelper.java @@ -3,7 +3,6 @@ import ecdar.controllers.EcdarController; import ecdar.controllers.EdgeController; import ecdar.presentations.ComponentOperatorPresentation; -import ecdar.presentations.ComponentPresentation; import ecdar.presentations.Grid; import ecdar.utility.UndoRedoStack; import javafx.beans.property.BooleanProperty; @@ -19,8 +18,6 @@ import java.util.List; import java.util.function.Supplier; -import static ecdar.presentations.Grid.GRID_SIZE; - public class ItemDragHelper { public static class DragBounds { diff --git a/src/main/java/ecdar/utility/helpers/MouseCircular.java b/src/main/java/ecdar/utility/helpers/MouseCircular.java index 6814b108..3672f52b 100644 --- a/src/main/java/ecdar/utility/helpers/MouseCircular.java +++ b/src/main/java/ecdar/utility/helpers/MouseCircular.java @@ -1,13 +1,10 @@ package ecdar.utility.helpers; import ecdar.controllers.EcdarController; -import ecdar.presentations.Grid; import ecdar.utility.mouse.MouseTracker; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; -import static ecdar.presentations.Grid.GRID_SIZE; - public class MouseCircular implements Circular { private final DoubleProperty x = new SimpleDoubleProperty(0d); private final DoubleProperty y = new SimpleDoubleProperty(0d); diff --git a/src/main/java/ecdar/utility/helpers/StringHelper.java b/src/main/java/ecdar/utility/helpers/StringHelper.java new file mode 100644 index 00000000..e158a4fb --- /dev/null +++ b/src/main/java/ecdar/utility/helpers/StringHelper.java @@ -0,0 +1,11 @@ +package ecdar.utility.helpers; + +public final class StringHelper { + public static String ConvertSymbolsToUnicode(String stringToReplace){ + return stringToReplace.replace(">=","\u2265").replace("<=","\u2264"); + } + + public static String ConvertUnicodeToSymbols(String stringToReplace){ + return stringToReplace.replace("\u2264","<=").replace("\u2265",">="); + } +} diff --git a/src/main/proto b/src/main/proto index 476f1afd..22f7b52a 160000 --- a/src/main/proto +++ b/src/main/proto @@ -1 +1 @@ -Subproject commit 476f1afd62596ec6a841227a3caa5fd40abc9c6e +Subproject commit 22f7b52a65a7dcf56563eb7b5d353ce4c47dd231 diff --git a/src/main/resources/ecdar/main.css b/src/main/resources/ecdar/main.css index 67af8bb5..0d99400f 100644 --- a/src/main/resources/ecdar/main.css +++ b/src/main/resources/ecdar/main.css @@ -290,6 +290,24 @@ -fx-font-size: 1em; } +.large-toggle-button { + -jfx-toggle-color:#dddddd; -jfx-untoggle-color:#dddddd; + -jfx-toggle-line-color:#7C8B92; -jfx-untoggle-line-color:#7C8B92; + -fx-opacity: 0.5; + -jfx-size: 1.6em; + -fx-font-size: 1em; +} + +.editor-simulator-toggle { + -fx-min-width: 8em; + -fx-max-width: 8em; + -fx-min-height: 4em; + -fx-max-height: 4em; + -fx-background-color: -blue-grey-800; + -fx-border-radius: 0 0 10 10; + -fx-background-radius: 0 0 10 10; +} + /* Response sizing for elements (icons with these classes will be scaled when scaling changes) */ .icon-size-medium { -fx-font-family: "Material Icons Regular"; @@ -347,4 +365,4 @@ .responsive-circle-radius { -fx-scale-x: 1em; -fx-scale-y: 1em; -} \ No newline at end of file +} diff --git a/src/main/resources/ecdar/presentations/EcdarPresentation.fxml b/src/main/resources/ecdar/presentations/EcdarPresentation.fxml index c7032ddc..87a3843b 100644 --- a/src/main/resources/ecdar/presentations/EcdarPresentation.fxml +++ b/src/main/resources/ecdar/presentations/EcdarPresentation.fxml @@ -18,90 +18,17 @@ fx:controller="ecdar.controllers.EcdarController">
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
- - - + - - - + @@ -116,7 +43,8 @@ - + @@ -168,12 +96,14 @@ - + - + @@ -184,13 +114,14 @@ - + - + @@ -205,7 +136,7 @@ - + @@ -245,6 +176,19 @@ + + + + + + + + + + + + @@ -292,8 +236,21 @@ - - + + + + + + + + + + + + + + + - - + + @@ -535,4 +492,9 @@ + + + + + diff --git a/src/main/resources/ecdar/presentations/EditorPresentation.fxml b/src/main/resources/ecdar/presentations/EditorPresentation.fxml new file mode 100644 index 00000000..bf3faac1 --- /dev/null +++ b/src/main/resources/ecdar/presentations/EditorPresentation.fxml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/ecdar/presentations/LeftSimPanePresentation.fxml b/src/main/resources/ecdar/presentations/LeftSimPanePresentation.fxml new file mode 100755 index 00000000..e80df29e --- /dev/null +++ b/src/main/resources/ecdar/presentations/LeftSimPanePresentation.fxml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/ecdar/presentations/MessageTabPanePresentation.fxml b/src/main/resources/ecdar/presentations/MessageTabPanePresentation.fxml deleted file mode 100644 index c1693929..00000000 --- a/src/main/resources/ecdar/presentations/MessageTabPanePresentation.fxml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/ecdar/presentations/ProcessPresentation.fxml b/src/main/resources/ecdar/presentations/ProcessPresentation.fxml new file mode 100755 index 00000000..6b4b8f77 --- /dev/null +++ b/src/main/resources/ecdar/presentations/ProcessPresentation.fxml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + + +
+
+
+ +
+
+ + + + + + +
+
+
\ No newline at end of file diff --git a/src/main/resources/ecdar/presentations/ProjectPanePresentation.fxml b/src/main/resources/ecdar/presentations/ProjectPanePresentation.fxml index 342405e6..5d9ea187 100644 --- a/src/main/resources/ecdar/presentations/ProjectPanePresentation.fxml +++ b/src/main/resources/ecdar/presentations/ProjectPanePresentation.fxml @@ -10,7 +10,8 @@ + fx:controller="ecdar.controllers.ProjectPaneController" + StackPane.alignment="TOP_LEFT"> @@ -56,7 +57,7 @@ - + @@ -68,8 +69,8 @@ - + diff --git a/src/main/resources/ecdar/presentations/QueryPanePresentation.fxml b/src/main/resources/ecdar/presentations/QueryPanePresentation.fxml index 6a984c1b..e50fc7bd 100644 --- a/src/main/resources/ecdar/presentations/QueryPanePresentation.fxml +++ b/src/main/resources/ecdar/presentations/QueryPanePresentation.fxml @@ -9,7 +9,8 @@ + fx:controller="ecdar.controllers.QueryPaneController" + StackPane.alignment="TOP_RIGHT"> diff --git a/src/main/resources/ecdar/presentations/QueryPresentation.fxml b/src/main/resources/ecdar/presentations/QueryPresentation.fxml index 223e15df..6b89a210 100644 --- a/src/main/resources/ecdar/presentations/QueryPresentation.fxml +++ b/src/main/resources/ecdar/presentations/QueryPresentation.fxml @@ -12,11 +12,11 @@ HBox.hgrow="ALWAYS" alignment="CENTER" fx:controller="ecdar.controllers.QueryController"> - + - + @@ -33,7 +33,7 @@ -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Ignored outputs on left side - - - - - - - - - - Ignored inputs on right side - - - - - - - - diff --git a/src/main/resources/ecdar/presentations/RightSimPanePresentation.fxml b/src/main/resources/ecdar/presentations/RightSimPanePresentation.fxml new file mode 100755 index 00000000..6f3d588c --- /dev/null +++ b/src/main/resources/ecdar/presentations/RightSimPanePresentation.fxml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/ecdar/presentations/SimEdgePresentation.fxml b/src/main/resources/ecdar/presentations/SimEdgePresentation.fxml new file mode 100755 index 00000000..b1224493 --- /dev/null +++ b/src/main/resources/ecdar/presentations/SimEdgePresentation.fxml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/ecdar/presentations/SimLocationPresentation.fxml b/src/main/resources/ecdar/presentations/SimLocationPresentation.fxml new file mode 100755 index 00000000..eb6b4543 --- /dev/null +++ b/src/main/resources/ecdar/presentations/SimLocationPresentation.fxml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/ecdar/presentations/SimNailPresentation.fxml b/src/main/resources/ecdar/presentations/SimNailPresentation.fxml new file mode 100755 index 00000000..17a63895 --- /dev/null +++ b/src/main/resources/ecdar/presentations/SimNailPresentation.fxml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/ecdar/presentations/SimTagPresentation.fxml b/src/main/resources/ecdar/presentations/SimTagPresentation.fxml new file mode 100755 index 00000000..f9e40d71 --- /dev/null +++ b/src/main/resources/ecdar/presentations/SimTagPresentation.fxml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/ecdar/presentations/SimulationInitializationDialogPresentation.fxml b/src/main/resources/ecdar/presentations/SimulationInitializationDialogPresentation.fxml new file mode 100644 index 00000000..eb97bdd1 --- /dev/null +++ b/src/main/resources/ecdar/presentations/SimulationInitializationDialogPresentation.fxml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + Simulation Initialization + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/ecdar/presentations/SimulatorOverviewPresentation.fxml b/src/main/resources/ecdar/presentations/SimulatorOverviewPresentation.fxml new file mode 100755 index 00000000..f854ef33 --- /dev/null +++ b/src/main/resources/ecdar/presentations/SimulatorOverviewPresentation.fxml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/ecdar/presentations/SimulatorPresentation.fxml b/src/main/resources/ecdar/presentations/SimulatorPresentation.fxml new file mode 100755 index 00000000..7524a380 --- /dev/null +++ b/src/main/resources/ecdar/presentations/SimulatorPresentation.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/src/main/resources/ecdar/presentations/TracePaneElementPresentation.fxml b/src/main/resources/ecdar/presentations/TracePaneElementPresentation.fxml new file mode 100755 index 00000000..0a233345 --- /dev/null +++ b/src/main/resources/ecdar/presentations/TracePaneElementPresentation.fxml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/ecdar/presentations/TransitionPaneElementPresentation.fxml b/src/main/resources/ecdar/presentations/TransitionPaneElementPresentation.fxml new file mode 100755 index 00000000..8047bcb3 --- /dev/null +++ b/src/main/resources/ecdar/presentations/TransitionPaneElementPresentation.fxml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/ecdar/presentations/TransitionPresentation.fxml b/src/main/resources/ecdar/presentations/TransitionPresentation.fxml new file mode 100755 index 00000000..900aa8d4 --- /dev/null +++ b/src/main/resources/ecdar/presentations/TransitionPresentation.fxml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/src/test/java/ecdar/TestFXBase.java b/src/test/java/ecdar/TestFXBase.java new file mode 100644 index 00000000..843527ee --- /dev/null +++ b/src/test/java/ecdar/TestFXBase.java @@ -0,0 +1,32 @@ +package ecdar; + +import javafx.scene.input.KeyCode; +import javafx.scene.input.MouseButton; +import javafx.stage.Stage; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.testfx.api.FxRobot; +import org.testfx.api.FxToolkit; +import org.testfx.framework.junit.ApplicationTest; + +import java.util.concurrent.TimeoutException; + +public class TestFXBase extends ApplicationTest { + @BeforeAll + static void setUp() throws Exception { + ApplicationTest.launch(Ecdar.class); + } + + @Override + public void start(Stage stage) { + stage.show(); + } + + @AfterAll + public static void afterEachTest() throws TimeoutException { + FxToolkit.hideStage(); + FxRobot robot = new FxRobot(); + robot.release(new KeyCode[]{}); + robot.release(new MouseButton[]{}); + } +} diff --git a/src/test/java/ecdar/abstractions/QueryTest.java b/src/test/java/ecdar/abstractions/QueryTest.java new file mode 100644 index 00000000..88d1e9fa --- /dev/null +++ b/src/test/java/ecdar/abstractions/QueryTest.java @@ -0,0 +1,24 @@ +package ecdar.abstractions; + +import ecdar.Ecdar; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions; + +public class QueryTest { + @BeforeAll + static void setup() { + Ecdar.setUpForTest(); + } + + @Test + public void testGetQuery() { + //Test that the query string from the query textfield is decoded correctly for the backend to use it + final Query query = new Query("(Administration || Machine || Researcher) \u2264 Spec)", "comment", QueryState.RUNNING); + + String expected = "(Administration || Machine || Researcher) <= Spec)"; + String result = query.getQuery(); + + Assertions.assertEquals(expected, result); + } +} diff --git a/src/test/java/ecdar/mutation/StrategyRuleTest.java b/src/test/java/ecdar/mutation/StrategyRuleTest.java index 2fad1c8c..5ed7144a 100644 --- a/src/test/java/ecdar/mutation/StrategyRuleTest.java +++ b/src/test/java/ecdar/mutation/StrategyRuleTest.java @@ -5,8 +5,6 @@ import org.junit.jupiter.api.Assertions; import java.util.HashMap; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class StrategyRuleTest { diff --git a/src/test/java/ecdar/mutation/operators/ChangeTargetOperatorTest.java b/src/test/java/ecdar/mutation/operators/ChangeTargetOperatorTest.java index 102ef715..fe3a869e 100644 --- a/src/test/java/ecdar/mutation/operators/ChangeTargetOperatorTest.java +++ b/src/test/java/ecdar/mutation/operators/ChangeTargetOperatorTest.java @@ -5,7 +5,6 @@ import ecdar.abstractions.Edge; import ecdar.abstractions.EdgeStatus; import ecdar.abstractions.Location; -import ecdar.mutation.operators.ChangeTargetOperator; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Assertions; diff --git a/src/test/java/ecdar/mutation/operators/SinkLocationOperatorTest.java b/src/test/java/ecdar/mutation/operators/SinkLocationOperatorTest.java index b4d9a492..8970f1f4 100644 --- a/src/test/java/ecdar/mutation/operators/SinkLocationOperatorTest.java +++ b/src/test/java/ecdar/mutation/operators/SinkLocationOperatorTest.java @@ -4,9 +4,8 @@ import ecdar.abstractions.Edge; import ecdar.abstractions.EdgeStatus; import ecdar.abstractions.Location; -import ecdar.mutation.operators.SinkLocationOperator; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; public class SinkLocationOperatorTest { diff --git a/src/test/java/ecdar/simulation/ReachabilityTest.java b/src/test/java/ecdar/simulation/ReachabilityTest.java new file mode 100644 index 00000000..6e645470 --- /dev/null +++ b/src/test/java/ecdar/simulation/ReachabilityTest.java @@ -0,0 +1,119 @@ +package ecdar.simulation; + +import ecdar.Ecdar; +import ecdar.abstractions.Component; +import ecdar.abstractions.Location; +import ecdar.backend.BackendHelper; +import ecdar.controllers.SimulationInitializationDialogController; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.*; + +public class ReachabilityTest { + + @BeforeAll + static void setup() { + Ecdar.setUpForTest(); + } + + @Test + void reachabilityQuerySyntaxTestSuccess() { + var regex = "query\\s+\\->\\s+\\[(\\w*)(,(\\w)*)*\\]\\([a-zA-Z0-9_<>=]*\\)(;\\[(\\w*)(,(\\w)*)\\]\\([a-zA-Z0-9_<>=]*\\))*"; + + var location = new Location(); + location.setId("L1"); + var component = new Component(); + component.setName("C1"); + + SimulationInitializationDialogController.ListOfComponents.clear(); + SimulationInitializationDialogController.ListOfComponents.add("C1"); + SimulationInitializationDialogController.ListOfComponents.add("C2"); + SimulationInitializationDialogController.ListOfComponents.add("C3"); + + var result = BackendHelper.getLocationReachableQuery(location, component, "query"); + assertTrue(result.matches(regex)); + } + + @Test + void reachabilityQueryLocationPosition1TestSuccess() { + var location = new Location(); + location.setId("L1"); + var component = new Component(); + component.setName("C1"); + + SimulationInitializationDialogController.ListOfComponents.clear(); + SimulationInitializationDialogController.ListOfComponents.add("C1"); + SimulationInitializationDialogController.ListOfComponents.add("C2"); + SimulationInitializationDialogController.ListOfComponents.add("C3"); + + var result = BackendHelper.getLocationReachableQuery(location, component, "query"); + var indexOfLocation = result.indexOf('[') + 1; + var output = result.charAt(indexOfLocation); + assertEquals(output, location.getId().charAt(0)); + } + + @Test + void reachabilityQueryLocationPosition2TestSuccess() { + var location = new Location(); + location.setId("L1"); + var component = new Component(); + component.setName("C1"); + + SimulationInitializationDialogController.ListOfComponents.clear(); + SimulationInitializationDialogController.ListOfComponents.add("C2"); + SimulationInitializationDialogController.ListOfComponents.add("C1"); + SimulationInitializationDialogController.ListOfComponents.add("C3"); + + var result = BackendHelper.getLocationReachableQuery(location, component, "query"); + var indexOfLocation = result.indexOf(',') + 1; + var output = result.charAt(indexOfLocation); + assertEquals(output, location.getId().charAt(0)); + } + + @Test + void reachabilityQueryLocationPosition3TestSuccess() { + var location = new Location(); + location.setId("L1"); + var component = new Component(); + component.setName("C1"); + + SimulationInitializationDialogController.ListOfComponents.clear(); + SimulationInitializationDialogController.ListOfComponents.add("C2"); + SimulationInitializationDialogController.ListOfComponents.add("C3"); + SimulationInitializationDialogController.ListOfComponents.add("C1"); + + var query = BackendHelper.getLocationReachableQuery(location, component, "query"); + var indexOfLocation = query.indexOf(']') - 2; + var output = query.charAt(indexOfLocation); + assertEquals(output, location.getId().charAt(0)); + } + + @Test + void reachabilityQueryNumberOfLocationsTestSuccess() { + var location = new Location(); + location.setId("L1"); + var component = new Component(); + component.setName("C1"); + + SimulationInitializationDialogController.ListOfComponents.clear(); + SimulationInitializationDialogController.ListOfComponents.add("C2"); + SimulationInitializationDialogController.ListOfComponents.add("C1"); + SimulationInitializationDialogController.ListOfComponents.add("C3"); + SimulationInitializationDialogController.ListOfComponents.add("C4"); + + var query = BackendHelper.getLocationReachableQuery(location, component, "query"); + int underscoreCount = 0; + for (int i = 0; i < query.length(); i++) { + if (query.charAt(i) == '_') { + underscoreCount++; + } + } + + assertEquals(SimulationInitializationDialogController.ListOfComponents.size(), underscoreCount + 1); + } +} diff --git a/src/test/java/ecdar/simulation/SimulationTest.java b/src/test/java/ecdar/simulation/SimulationTest.java new file mode 100644 index 00000000..c0584f3a --- /dev/null +++ b/src/test/java/ecdar/simulation/SimulationTest.java @@ -0,0 +1,108 @@ +package ecdar.simulation; + +import EcdarProtoBuf.EcdarBackendGrpc; +import EcdarProtoBuf.ObjectProtos; +import EcdarProtoBuf.QueryProtos; +import EcdarProtoBuf.ObjectProtos.DecisionPoint; +import EcdarProtoBuf.ObjectProtos.LocationTuple; +import EcdarProtoBuf.ObjectProtos.SpecificComponent; +import ecdar.abstractions.Component; +import ecdar.abstractions.Location; +import io.grpc.BindableService; +import io.grpc.ManagedChannel; +import io.grpc.Status; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcCleanupRule; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions; + +public class SimulationTest { + public GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + private final String serverName = InProcessServerBuilder.generateName(); + + @Test + public void testGetInitialStateHighlightsTheInitialLocation() { + final List components = generateComponentsWithInitialLocations(); + + BindableService testService = new EcdarBackendGrpc.EcdarBackendImplBase() { + @Override + public void startSimulation(QueryProtos.SimulationStartRequest request, + StreamObserver responseObserver) { + try { + ObjectProtos.LocationTuple locations = LocationTuple.newBuilder().addAllLocations(components.stream() + .map(c -> ObjectProtos.Location.newBuilder() + .setSpecificComponent(SpecificComponent.newBuilder() + .setComponentName(c.getName())) + .setId(c.getInitialLocation().getId()) + .build()) + .collect(Collectors.toList())) + .build(); + + ObjectProtos.State state = ObjectProtos.State.newBuilder().setLocationTuple(locations).build(); + DecisionPoint decisionPoint = DecisionPoint.newBuilder().setSource(state).build(); + QueryProtos.SimulationStepResponse response = QueryProtos.SimulationStepResponse.newBuilder() + .setNewDecisionPoint(decisionPoint) + .build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + } catch (Throwable e) { + responseObserver.onError(Status.INVALID_ARGUMENT.withDescription(e.getMessage()).asException()); + } + } + + @Override + public void takeSimulationStep(EcdarProtoBuf.QueryProtos.SimulationStepRequest request, + io.grpc.stub.StreamObserver responseObserver) { + } + }; + + final ManagedChannel channel; + final EcdarBackendGrpc.EcdarBackendBlockingStub stub; + try { + grpcCleanup.register(InProcessServerBuilder + .forName(serverName).directExecutor().addService(testService).build().start()); + channel = grpcCleanup.register(InProcessChannelBuilder + .forName(serverName).directExecutor().build()); + stub = EcdarBackendGrpc.newBlockingStub(channel); + + QueryProtos.SimulationStartRequest request = QueryProtos.SimulationStartRequest.newBuilder().build(); + + var expectedResponse = new ObjectProtos.Location[components.size()]; + + for (int i = 0; i < components.size(); i++) { + Component comp = components.get(i); + expectedResponse[i] = ObjectProtos.Location.newBuilder() + .setSpecificComponent(SpecificComponent.newBuilder().setComponentName(comp.getName())) + .setId(comp.getInitialLocation().getId()).build(); + } + + var result = stub.startSimulation(request).getNewDecisionPoint().getSource().getLocationTuple().getLocationsList().toArray(); + + Assertions.assertArrayEquals(expectedResponse, result); + } catch (IOException e) { + Assertions.fail("Exception encountered: " + e.getMessage()); + } + } + + private List generateComponentsWithInitialLocations() { + List comps = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + var comp = new Component(); + comp.setName(comp + "_" + i); + var loc = new Location(comp + "_initial"); + loc.setType(Location.Type.INITIAL); + comp.addLocation(loc); + comps.add(comp); + } + + return comps; + } +} diff --git a/src/test/java/ecdar/ui/SidePaneTest.java b/src/test/java/ecdar/ui/SidePaneTest.java index 73214791..b38a65de 100644 --- a/src/test/java/ecdar/ui/SidePaneTest.java +++ b/src/test/java/ecdar/ui/SidePaneTest.java @@ -11,8 +11,6 @@ import org.testfx.util.WaitForAsyncUtils; import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Predicate; diff --git a/src/test/java/ecdar/utility/StringHelperTest.java b/src/test/java/ecdar/utility/StringHelperTest.java new file mode 100644 index 00000000..36cef770 --- /dev/null +++ b/src/test/java/ecdar/utility/StringHelperTest.java @@ -0,0 +1,45 @@ +package ecdar.utility; + +import ecdar.utility.helpers.StringHelper; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class StringHelperTest { + @ParameterizedTest + @ValueSource(strings = { + "2 >= 4;2 \u2265 4", + "2 <= 4;2 \u2264 4", + "2 == 4;2 == 4", + "2 > 4;2 > 4", + "2 < 4;2 < 4", + }) + void convertSymbolsToUnicode(String inputAndExpectedOutput) { + var split = inputAndExpectedOutput.split(";"); + var input = split[0]; + var expectedOutput = split[1]; + + var actualOutput = StringHelper.ConvertSymbolsToUnicode(input); + + assertEquals(expectedOutput, actualOutput); + } + + @ParameterizedTest + @ValueSource(strings = { + "2 \u2265 4;2 >= 4", + "2 \u2264 4;2 <= 4", + "2 == 4;2 == 4", + "2 > 4;2 > 4", + "2 < 4;2 < 4" + }) + void convertUnicodeToSymbols(String inputAndExpectedOutput) { + var split = inputAndExpectedOutput.split(";"); + var input = split[0]; + var expectedOutput = split[1]; + + var actualOutput = StringHelper.ConvertUnicodeToSymbols(input); + + assertEquals(expectedOutput, actualOutput); + } +}