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 1961aba0..e3e38985 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ lib/ .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 86911063..74ecff58 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,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 @@ -47,7 +61,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' @@ -76,6 +89,7 @@ dependencies { testImplementation "io.grpc:grpc-testing:${grpcVersion}" testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.0.0-M4' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' testImplementation 'org.testfx:testfx-junit:4.0.15-alpha' testImplementation 'org.testfx:openjfx-monocle:jdk-12.0.1+2' @@ -83,7 +97,6 @@ dependencies { testImplementation 'org.mockito:mockito-junit-jupiter:4.8.0' } - test { useJUnitPlatform { includeEngines 'junit-jupiter' } diff --git a/src/main/java/ecdar/Ecdar.java b/src/main/java/ecdar/Ecdar.java index ccf17518..362407c1 100644 --- a/src/main/java/ecdar/Ecdar.java +++ b/src/main/java/ecdar/Ecdar.java @@ -4,7 +4,6 @@ import ecdar.backend.BackendException; import ecdar.backend.BackendHelper; import ecdar.code_analysis.CodeAnalysis; -import ecdar.controllers.EcdarController; import ecdar.issues.ExitStatusCodes; import ecdar.presentations.*; import ecdar.utility.keyboard.Keybind; @@ -135,8 +134,8 @@ public static void showHelp() { presentation.showHelp(); } - public static BooleanProperty toggleFilePane() { - return presentation.toggleFilePane(); + public static BooleanProperty toggleLeftPane() { + return presentation.toggleLeftPane(); } /** @@ -163,7 +162,7 @@ public static BooleanProperty toggleRunBackgroundQueries() { } public static BooleanProperty toggleQueryPane() { - return presentation.toggleQueryPane(); + return presentation.toggleRightPane(); } public static BooleanProperty isSplitProperty() { @@ -256,8 +255,6 @@ public void start(final Stage stage) { // We're now ready! Let the curtains fall! stage.show(); - EcdarController.reachabilityServiceEnabled = true; - // Register a key-bind for showing debug-information KeyboardTracker.registerKeybind("DEBUG", new Keybind(new KeyCodeCombination(KeyCode.F12), () -> { // Toggle the debug mode for the debug class (will update misc. debug variables which presentations bind to) @@ -311,7 +308,7 @@ public void start(final Stage stage) { } }); - project = presentation.getController().projectPane.getController().project; + project = presentation.getController().getEditorPresentation().getController().projectPane.getController().project; } private void loadFonts() { @@ -378,22 +375,10 @@ public static void initializeProjectFolder() throws IOException { // If we found a component set that as active serializationDone = true; - - // Update reachability check timer when components change - getProject().getComponents().addListener((ListChangeListener) c -> { - while (c.next()) { - c.getAddedSubList().forEach(component -> { - component.getLocations().addListener((ListChangeListener) loc -> EcdarController.runReachabilityAnalysis()); - component.getDisplayableEdges().addListener((ListChangeListener) de -> EcdarController.runReachabilityAnalysis()); - component.declarationsTextProperty().addListener((observable, oldValue, newValue) -> EcdarController.runReachabilityAnalysis()); - component.includeInPeriodicCheckProperty().addListener((observable, oldValue, newValue) -> EcdarController.runReachabilityAnalysis()); - }); - } - }); } public static ComponentPresentation getComponentPresentationOfComponent(Component component) { - return getPresentation().getController().projectPane.getController().getComponentPresentations().stream().filter(componentPresentation -> componentPresentation.getController().getComponent().equals(component)).findFirst().orElse(null); + return getPresentation().getController().getEditorPresentation().getController().projectPane.getController().getComponentPresentations().stream().filter(componentPresentation -> componentPresentation.getController().getComponent().equals(component)).findFirst().orElse(null); } private static String getVersion() { diff --git a/src/main/java/ecdar/abstractions/ClockConstraint.java b/src/main/java/ecdar/abstractions/ClockConstraint.java new file mode 100644 index 00000000..7dff0fcd --- /dev/null +++ b/src/main/java/ecdar/abstractions/ClockConstraint.java @@ -0,0 +1,31 @@ +package ecdar.abstractions; + +import javafx.util.Pair; + +public class ClockConstraint { + public final Pair clocks; + public final char comparator; + public final int constant; + public final boolean isStrict; + + public ClockConstraint(String leftClock, String rightClock, int constant, char comparator, boolean isStrict) { + this.clocks = new Pair<>(leftClock, rightClock); + this.constant = constant; + this.comparator = comparator; + this.isStrict = isStrict; + } + + @Override + public String toString() { + if (clocks.getValue() != null) { + return clocks.getKey() + " - " + + clocks.getValue() + " " + + comparator + (isStrict ? "= " : " ") + + constant; + } else { + return clocks.getKey() + " " + + comparator + (isStrict ? "= " : " ") + + constant; + } + } +} \ No newline at end of file diff --git a/src/main/java/ecdar/abstractions/Component.java b/src/main/java/ecdar/abstractions/Component.java index ccc438a8..b83808a2 100644 --- a/src/main/java/ecdar/abstractions/Component.java +++ b/src/main/java/ecdar/abstractions/Component.java @@ -1,7 +1,6 @@ package ecdar.abstractions; import ecdar.utility.UndoRedoStack; -import ecdar.utility.colors.Color; import ecdar.utility.colors.EnabledColor; import ecdar.utility.helpers.Boxed; import ecdar.utility.helpers.MouseCircular; @@ -12,7 +11,6 @@ import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; -import javafx.util.Pair; import java.util.*; import java.util.regex.Matcher; @@ -31,11 +29,15 @@ public class Component extends HighLevelModel implements Boxed { // Verification properties private final ObservableList locations = FXCollections.observableArrayList(); - private final ObservableList displayableEdges = 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 List failingIOStrings = new ArrayList(); private final StringProperty description = new SimpleStringProperty(""); - private final StringProperty declarationsText = new SimpleStringProperty(""); + private final StringProperty declarationsText = new SimpleStringProperty("");; + private BooleanProperty isFailing = new SimpleBooleanProperty(false); // Background check private final BooleanProperty includeInPeriodicCheck = new SimpleBooleanProperty(true); @@ -45,12 +47,17 @@ public class Component extends HighLevelModel implements Boxed { private final BooleanProperty declarationOpen = new SimpleBooleanProperty(false); public Location previousLocationForDraggedEdge; - + public boolean getIsFailing(){return isFailing.get();} + public BooleanProperty getIsFailingProperty(){return isFailing;} + public void setIsFailing(boolean isFailingInput){this.isFailing.set(isFailingInput);} + public List getFailingIOStrings(){return failingIOStrings;} + public void setFailingIOStrings(List failingIOStrings){ + this.failingIOStrings.clear(); + this.failingIOStrings.addAll(failingIOStrings);} /** * Constructs an empty component */ public Component() { - } /** @@ -88,7 +95,7 @@ public Component(final JsonObject json) { private void initializeIOListeners() { final ChangeListener listener = (observable, oldValue, newValue) -> updateIOList(); - displayableEdges.addListener((ListChangeListener) c -> { + edges.addListener((ListChangeListener) c -> { // Update the list so empty I/O status is also added to I/OLists updateIOList(); @@ -191,12 +198,73 @@ public String getUniqueLocationId() { } } + /** + * 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 */ public ObservableList getDisplayableEdges() { - return displayableEdges; + return edges; } /** @@ -204,16 +272,16 @@ public ObservableList getDisplayableEdges() { * @return All functional edges of the component */ public List getEdges() { - return getListOfEdgesFromDisplayableEdges(displayableEdges); + return getListOfEdgesFromDisplayableEdges(edges); } public boolean addEdge(final DisplayableEdge edge) { - if (displayableEdges.contains(edge)) return false; - return displayableEdges.add(edge); + if (edges.contains(edge)) return false; + return edges.add(edge); } public boolean removeEdge(final DisplayableEdge edge) { - return displayableEdges.remove(edge); + return edges.remove(edge); } /** @@ -224,7 +292,7 @@ public boolean removeEdge(final DisplayableEdge edge) { public List getRelatedEdges(final Location location) { final ArrayList relatedEdges = new ArrayList<>(); - displayableEdges.forEach(edge -> { + edges.forEach(edge -> { if(location.equals(edge.getSourceLocation()) || location.equals(edge.getTargetLocation())) { relatedEdges.add(edge); } @@ -305,7 +373,7 @@ public void setInitialLocation(final Location initialLocation) { } public DisplayableEdge getUnfinishedEdge() { - for (final DisplayableEdge edge : displayableEdges) { + for (final DisplayableEdge edge : edges) { if (edge.getTargetLocation() == null || edge.getSourceCircular() instanceof MouseCircular || edge.getTargetCircular() instanceof MouseCircular) @@ -469,7 +537,7 @@ public void updateIOList() { final List localInputStrings = new ArrayList<>(); final List localOutputStrings = new ArrayList<>(); - List edgeList = getListOfEdgesFromDisplayableEdges(displayableEdges); + List edgeList = getListOfEdgesFromDisplayableEdges(edges); for (final Edge edge : edgeList) { // Extract channel id based on UPPAAL id definition @@ -509,7 +577,7 @@ public JsonObject serialize() { result.add(LOCATIONS, locations); final JsonArray edges = new JsonArray(); - getListOfEdgesFromDisplayableEdges(this.displayableEdges).forEach(edge -> edges.add(edge.serialize())); + getListOfEdgesFromDisplayableEdges(this.edges).forEach(edge -> edges.add(edge.serialize())); result.add(EDGES, edges); result.addProperty(DESCRIPTION, getDescription()); @@ -542,7 +610,7 @@ public void deserialize(final JsonObject json) { if (!edgeGroup.isEmpty()) { GroupedEdge groupedEdge = null; - for (DisplayableEdge edge : displayableEdges) { + for (DisplayableEdge edge : edges) { if (edge instanceof GroupedEdge && edge.getId().equals(edgeGroup)) { groupedEdge = ((GroupedEdge) edge); break; @@ -553,18 +621,18 @@ public void deserialize(final JsonObject json) { GroupedEdge newGroupedEdge = new GroupedEdge(newEdge); newGroupedEdge.setId(edgeGroup); - displayableEdges.add(newGroupedEdge); + edges.add(newGroupedEdge); } else { boolean hasSameGuardAndUpdate = groupedEdge.addEdgeToGroup(newEdge); if (!hasSameGuardAndUpdate) { // The edge has the same group id as another edge, but has different guard and/or update newEdge.setGroup(""); - displayableEdges.add(newEdge); + edges.add(newEdge); } } } else { - displayableEdges.add(newEdge); + edges.add(newEdge); } }); diff --git a/src/main/java/ecdar/abstractions/Decision.java b/src/main/java/ecdar/abstractions/Decision.java new file mode 100644 index 00000000..3d659732 --- /dev/null +++ b/src/main/java/ecdar/abstractions/Decision.java @@ -0,0 +1,45 @@ +package ecdar.abstractions; + +import EcdarProtoBuf.ObjectProtos; +import EcdarProtoBuf.QueryProtos; +import ecdar.backend.GrpcRequest; +import ecdar.backend.GrpcRequestFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class Decision implements RequestSource { + public final String composition; + public final List edgeIds; + public final String action; + public final List clockConstraints; + public ObjectProtos.Decision protoDecision; + + public Decision(String composition, List edgeIds, String action, List clockConstraints, ObjectProtos.Decision protoDecision) { + this.composition = composition; + this.edgeIds = edgeIds; + this.action = action; + this.clockConstraints = clockConstraints; + this.protoDecision = protoDecision; + } + + /** + * This constructed is used to represent the decision submitted for the initial state + * ToDo NIELS: refactor to use a factory pattern for constructing new decisions + */ + public Decision(String composition) { + this(composition, new ArrayList<>(), null, null, null); + } + + public boolean isInitial() { + return protoDecision == null; + } + + @Override + public GrpcRequest accept(GrpcRequestFactory requestFactory, + Consumer successConsumer, + Consumer errorConsumer) { + return requestFactory.create(this, successConsumer, errorConsumer); + } +} diff --git a/src/main/java/ecdar/abstractions/DisplayableEdge.java b/src/main/java/ecdar/abstractions/DisplayableEdge.java index 26096663..121e64f6 100644 --- a/src/main/java/ecdar/abstractions/DisplayableEdge.java +++ b/src/main/java/ecdar/abstractions/DisplayableEdge.java @@ -1,16 +1,16 @@ package ecdar.abstractions; -import ecdar.Ecdar; import ecdar.code_analysis.Nearable; -import ecdar.utility.colors.Color; import ecdar.utility.colors.EnabledColor; 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(""); @@ -38,6 +38,9 @@ public abstract class DisplayableEdge implements Nearable { private final BooleanProperty isHighlighted = new SimpleBooleanProperty(false); + protected final BooleanProperty failing = new SimpleBooleanProperty(false); + private final BooleanProperty isHighlightedForReachability = new SimpleBooleanProperty(false); + public Location getSourceLocation() { return sourceLocation.get(); } @@ -81,7 +84,7 @@ public StringProperty selectProperty() { } public String getGuard() { - return guard.get(); + return StringHelper.ConvertUnicodeToSymbols(guard.get()); } public void setGuard(final String guard) { @@ -118,9 +121,12 @@ public ObjectProperty colorProperty() { public void setIsHighlighted(final boolean highlight){ this.isHighlighted.set(highlight);} - public boolean getIsHighlighted(){ return this.isHighlighted.get(); } + public boolean isHighlighted(){ return this.isHighlighted.get(); } + public boolean getIsHighlightedForReachability(){ return this.isHighlightedForReachability.get(); } public BooleanProperty isHighlightedProperty() { return this.isHighlighted; } + public BooleanProperty isHighlightedForReachabilityProperty() { return this.isHighlightedForReachability; } + public ObservableList getNails() { return nails; @@ -232,6 +238,8 @@ public void switchStatus() { } } + public void setIsHighlightedForReachability(final boolean highlightedForReachability){ this.isHighlightedForReachability.set(highlightedForReachability);} + public enum PropertyType { NONE(-1), SELECTION(0), @@ -266,12 +274,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()); } /** @@ -289,4 +292,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/Edge.java b/src/main/java/ecdar/abstractions/Edge.java index 97adf5cb..754c9947 100644 --- a/src/main/java/ecdar/abstractions/Edge.java +++ b/src/main/java/ecdar/abstractions/Edge.java @@ -40,12 +40,10 @@ public Edge(final Location sourceLocation, final EdgeStatus status) { setId(); } - bindReachabilityAnalysis(); } public Edge(final JsonObject jsonObject, final Component component) { deserialize(jsonObject, component); - bindReachabilityAnalysis(); } public String getSync() { @@ -68,7 +66,7 @@ public String getSyncWithSymbol() { return sync.get() + (ioStatus.get().equals(EdgeStatus.INPUT) ? "?" : "!"); } - public String getGroup(){ + public String getGroup() { return group.get(); } @@ -76,6 +74,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 +205,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); @@ -223,15 +243,6 @@ public void deserialize(final JsonObject json, final Component component) { } } - private void bindReachabilityAnalysis() { - // If there is no EcdarPresentation, we are running tests and EcdarController calls will fail - if (Ecdar.getPresentation() == null) return; - selectProperty().addListener((observable, oldValue, newValue) -> EcdarController.runReachabilityAnalysis()); - guardProperty().addListener((observable, oldValue, newValue) -> EcdarController.runReachabilityAnalysis()); - syncProperty().addListener((observable, oldValue, newValue) -> EcdarController.runReachabilityAnalysis()); - updateProperty().addListener((observable, oldValue, newValue) -> EcdarController.runReachabilityAnalysis()); - } - /** * Adds a synchronization nail at (0, 0). * Adds a specified synchronization property to this edge. diff --git a/src/main/java/ecdar/abstractions/GroupedEdge.java b/src/main/java/ecdar/abstractions/GroupedEdge.java index be8c0765..10935628 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<>()); @@ -54,7 +54,7 @@ private void initializeFromEdge(Edge edge) { setGuard(edge.getGuard()); setUpdate(edge.getUpdate()); setColor(edge.getColor()); - setIsHighlighted(edge.getIsHighlighted()); + setIsHighlighted(edge.isHighlighted()); setIsLocked(edge.getIsLockedProperty().getValue()); setStatus(edge.getStatus()); } @@ -89,12 +89,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()); } /** @@ -120,7 +115,7 @@ public Edge getBaseSubEdge() { edge.guardProperty().bind(this.guardProperty()); edge.updateProperty().bind(this.updateProperty()); edge.colorProperty().bind(this.colorProperty()); - edge.setIsHighlighted(this.getIsHighlighted()); + edge.setIsHighlighted(this.isHighlighted()); edge.getIsLockedProperty().bind(this.getIsLockedProperty()); edge.setGroup(this.getId()); edge.makeSyncNailBetweenLocations(); @@ -166,6 +161,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 9fa06e75..8b5c458f 100644 --- a/src/main/java/ecdar/abstractions/Location.java +++ b/src/main/java/ecdar/abstractions/Location.java @@ -7,6 +7,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; @@ -54,13 +55,13 @@ 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() { } public Location(final String id) { setId(id); - bindReachabilityAnalysis(); } public Location(final Component component, final Type type, final String id, final double x, final double y){ @@ -83,7 +84,6 @@ public Location(final Component component, final Type type, final String id, fin public Location(final JsonObject jsonObject) { deserialize(jsonObject); - bindReachabilityAnalysis(); } /** @@ -91,7 +91,6 @@ public Location(final JsonObject jsonObject) { */ public void initialize(String id) { setId(id); - bindReachabilityAnalysis(); } /** @@ -148,7 +147,7 @@ public StringProperty idProperty() { } public String getInvariant() { - return invariant.get(); + return StringHelper.ConvertUnicodeToSymbols(invariant.get()); } public void setInvariant(final String invariant) { @@ -177,7 +176,6 @@ public Urgency getUrgency() { public void setUrgency(final Urgency urgency) { // If there is no EcdarPresentation, we are running tests and EcdarController calls will fail - if (Ecdar.getPresentation() != null) EcdarController.runReachabilityAnalysis(); this.urgency.set(urgency); } @@ -269,7 +267,6 @@ public double getInvariantY() { public void setInvariantY(final double invariantY) { // If there is no EcdarPresentation, we are running tests and EcdarController calls will fail - if (Ecdar.getPresentation() != null) EcdarController.runReachabilityAnalysis(); this.invariantY.set(invariantY); } @@ -425,6 +422,30 @@ 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 } @@ -437,11 +458,6 @@ public enum Reachability { REACHABLE, UNREACHABLE, UNKNOWN, EXCLUDED } - private void bindReachabilityAnalysis() { - invariantProperty().addListener((observable, oldValue, newValue) -> EcdarController.runReachabilityAnalysis()); - urgencyProperty().addListener((observable, oldValue, newValue) -> EcdarController.runReachabilityAnalysis()); - } - public boolean isUniversalOrInconsistent() { return getType().equals(Type.UNIVERSAL) || getType().equals(Type.INCONSISTENT); } diff --git a/src/main/java/ecdar/abstractions/Query.java b/src/main/java/ecdar/abstractions/Query.java index 93657221..9dc4dc36 100644 --- a/src/main/java/ecdar/abstractions/Query.java +++ b/src/main/java/ecdar/abstractions/Query.java @@ -5,18 +5,24 @@ import ecdar.Ecdar; import ecdar.backend.*; import ecdar.controllers.EcdarController; +import ecdar.controllers.SimulationController; import ecdar.utility.UndoRedoStack; import ecdar.utility.helpers.StringValidator; +import EcdarProtoBuf.ObjectProtos; +import ecdar.utility.helpers.StringHelper; import ecdar.utility.serialize.Serializable; import com.google.gson.JsonObject; import javafx.application.Platform; import javafx.beans.property.*; import javafx.collections.ObservableList; +import java.util.ArrayList; import java.util.NoSuchElementException; +import java.util.List; +import java.util.function.BiConsumer; import java.util.function.Consumer; -public class Query implements Serializable { +public class Query implements RequestSource, Serializable { private static final String QUERY = "query"; private static final String COMMENT = "comment"; private static final String IS_PERIODIC = "isPeriodic"; @@ -33,6 +39,11 @@ public class Query implements Serializable { private final Consumer successConsumer = (aBoolean) -> { if (aBoolean) { + for (Component c : Ecdar.getProject().getComponents()) { + c.removeFailingLocations(); + c.removeFailingEdges(); + c.setIsFailing(false); + } setQueryState(QueryState.SUCCESSFUL); } else { setQueryState(QueryState.ERROR); @@ -62,8 +73,43 @@ public class Query implements Serializable { } }; + private final BiConsumer> stateActionConsumer = (state, actions) -> { + // ToDo: Color all IO strings red + for (Component c : Ecdar.getProject().getComponents()) { + c.removeFailingLocations(); + c.removeFailingEdges(); + } + +// for (ObjectProtos.LeafLocation location : ) { +// Component c = Ecdar.getProject().findComponent(location.getSpecificComponent().getComponentName()); +// +// if (c == null) { +// throw new NullPointerException("Could not find the specific component: " + location.getSpecificComponent().getComponentName()); +// } +// +// if (location.getId().isEmpty()) { +// if (c.getName().equals(location.getSpecificComponent().getComponentName())) { +// c.setFailingIOStrings(actions); +// c.setIsFailing(true); +// } +// } else { +// 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 (actions.get(0).equals(edge.getSync()) && edge.getSourceLocation() == l) { +// c.addFailingEdge(edge); +// } +// } +// } +// } + }; + public Query(final String query, final String comment, final QueryState queryState, final Engine engine) { - this.query.set(query); + this.setQuery(query); this.comment.set(comment); this.queryState.set(queryState); setEngine(engine); @@ -90,7 +136,7 @@ public ObjectProperty queryStateProperty() { } public String getQuery() { - return query.get(); + return StringHelper.ConvertUnicodeToSymbols(this.query.get()); } public void setQuery(final String query) { @@ -113,7 +159,9 @@ public StringProperty commentProperty() { return comment; } - public StringProperty errors() { return errors; } + public StringProperty errors() { + return errors; + } public boolean isPeriodic() { return isPeriodic.get(); @@ -171,30 +219,65 @@ public void execute() throws NoSuchElementException { setQueryState(QueryState.RUNNING); errors().set(""); - getEngine().enqueueQuery(this, this::handleQueryResponse, this::handleQueryBackendError); + getEngine().enqueueRequest(this, this::handleQueryResponse, this::handleQueryBackendError); } private void handleQueryResponse(QueryProtos.QueryResponse value) { - // If the query has been cancelled, ignore the result if (getQueryState() == QueryState.UNKNOWN) return; - if (value.hasRefinement() && value.getRefinement().getSuccess()) { - setQueryState(QueryState.SUCCESSFUL); - getSuccessConsumer().accept(true); - } else if (value.hasConsistency() && value.getConsistency().getSuccess()) { + if (value.hasSuccess()) { setQueryState(QueryState.SUCCESSFUL); getSuccessConsumer().accept(true); - } else if (value.hasDeterminism() && value.getDeterminism().getSuccess()) { - setQueryState(QueryState.SUCCESSFUL); - getSuccessConsumer().accept(true); - } else if (value.hasComponent()) { - setQueryState(QueryState.SUCCESSFUL); - getSuccessConsumer().accept(true); - JsonObject returnedComponent = (JsonObject) JsonParser.parseString(value.getComponent().getComponent().getJson()); - addGeneratedComponent(new Component(returnedComponent)); + + switch (value.getResultCase()) { + case COMPONENT: + JsonObject returnedComponent = (JsonObject) JsonParser.parseString(value.getComponent().getJson()); + addGeneratedComponent(new Component(returnedComponent)); + break; + case REACHABILITY: + // Highlight edges in path + ArrayList edgeIds = new ArrayList<>(); + for (var decision : value.getReachabilityPath().getPath().getDecisionsList()) { + for (var edge : decision.getEdgesList()) { + edgeIds.add(edge.getId()); + } + } + +// highlightReachabilityEdges(edgeIds); // ToDo NIELS: Refactor + break; + } } else { setQueryState(QueryState.ERROR); + getFailureConsumer().accept(new BackendException.QueryErrorException(value.getError().getError())); getSuccessConsumer().accept(false); + + switch (value.getResultCase()) { + case REFINEMENT: + getStateActionConsumer().accept(value.getRefinement().getRefinementState().getState().getState(), + new ArrayList<>()); + break; + + case CONSISTENCY: + getStateActionConsumer().accept(value.getConsistency().getFailureState(), + new ArrayList<>()); + break; + + case DETERMINISM: + getStateActionConsumer().accept(value.getDeterminism().getFailureState().getState(), + new ArrayList<>()); + break; + + case IMPLEMENTATION: + getStateActionConsumer().accept(value.getImplementation().getFailureState().getState(), + new ArrayList<>()); + break; + + case REACHABILITY: + // ToDo: Reachability failure state not implemented + getStateActionConsumer().accept(value.getConsistency().getFailureState(), + new ArrayList<>()); + break; + } } } @@ -202,6 +285,11 @@ private void handleQueryBackendError(Throwable t) { // If the query has been cancelled, ignore the error if (getQueryState() == QueryState.UNKNOWN) return; + // Due to limit information provided by the engines, we can only show the following for unreachable locations + if (getType() == QueryType.REACHABILITY) { + Ecdar.showToast("Timeout (no response from backend): 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]; @@ -222,7 +310,7 @@ private void addGeneratedComponent(Component newComponent) { Platform.runLater(() -> { newComponent.setTemporary(true); - ObservableList listOfGeneratedComponents = Ecdar.getProject().getTempComponents(); // ToDo NIELS: Refactor + ObservableList listOfGeneratedComponents = Ecdar.getProject().getTempComponents(); Component matchedComponent = null; for (Component currentGeneratedComponent : listOfGeneratedComponents) { @@ -258,6 +346,35 @@ private void addGeneratedComponent(Component newComponent) { }); } + /** + * Getter for the state action consumer. + * + * @return The State Consumer + */ + public BiConsumer> getStateActionConsumer() { + return stateActionConsumer; + } + + public void cancel() { + if (getQueryState().equals(QueryState.RUNNING)) { + forcedCancel = true; + setQueryState(QueryState.UNKNOWN); + } + } + + public void addError(String e) { + errors.set(errors.getValue() + e + "\n"); + } + + public String getCurrentErrors() { + return errors.getValue(); + } + + @Override + public GrpcRequest accept(GrpcRequestFactory requestFactory, Consumer successConsumer, Consumer errorConsumer) { + return requestFactory.create(this, this::handleQueryResponse, this::handleQueryBackendError); + } + @Override public JsonObject serialize() { final JsonObject result = new JsonObject(); @@ -274,7 +391,7 @@ public JsonObject serialize() { public void deserialize(final JsonObject json) { String query = json.getAsJsonPrimitive(QUERY).getAsString(); - if(query.contains(":")) { + if (query.contains(":")) { String[] queryFieldFromJSON = json.getAsJsonPrimitive(QUERY).getAsString().split(": "); setType(QueryType.fromString(queryFieldFromJSON[0])); setQuery(queryFieldFromJSON[1]); @@ -288,25 +405,10 @@ public void deserialize(final JsonObject json) { setIsPeriodic(json.getAsJsonPrimitive(IS_PERIODIC).getAsBoolean()); } - if(json.has(ENGINE)) { + if (json.has(ENGINE)) { setEngine(BackendHelper.getEngineByName(json.getAsJsonPrimitive(ENGINE).getAsString())); } else { setEngine(BackendHelper.getDefaultEngine()); } } - - public void cancel() { - if (getQueryState().equals(QueryState.RUNNING)) { - forcedCancel = true; - setQueryState(QueryState.UNKNOWN); - } - } - - public void addError(String e) { - errors.set(errors.getValue() + e + "\n"); - } - - public String getCurrentErrors() { - return errors.getValue(); - } } diff --git a/src/main/java/ecdar/abstractions/QueryType.java b/src/main/java/ecdar/abstractions/QueryType.java index 5b8e0b74..4552cffb 100644 --- a/src/main/java/ecdar/abstractions/QueryType.java +++ b/src/main/java/ecdar/abstractions/QueryType.java @@ -2,7 +2,8 @@ public enum QueryType { REACHABILITY("reachability", "E<>"), - REFINEMENT("refinement", "<="), + REFINEMENT("refinement", "\u2264"), + QUOTIENT("quotient", "\\"), SPECIFICATION("specification", "Spec"), IMPLEMENTATION("implementation", "Imp"), CONSISTENCY("consistency", "Con"), diff --git a/src/main/java/ecdar/abstractions/RequestSource.java b/src/main/java/ecdar/abstractions/RequestSource.java new file mode 100644 index 00000000..ebfb7720 --- /dev/null +++ b/src/main/java/ecdar/abstractions/RequestSource.java @@ -0,0 +1,10 @@ +package ecdar.abstractions; + +import ecdar.backend.GrpcRequest; +import ecdar.backend.GrpcRequestFactory; + +import java.util.function.Consumer; + +public interface RequestSource { + GrpcRequest accept(GrpcRequestFactory requestFactory, Consumer successConsumer, Consumer errorConsumer); +} diff --git a/src/main/java/ecdar/abstractions/Simulation.java b/src/main/java/ecdar/abstractions/Simulation.java new file mode 100644 index 00000000..1fc45674 --- /dev/null +++ b/src/main/java/ecdar/abstractions/Simulation.java @@ -0,0 +1,63 @@ +package ecdar.abstractions; + +import ecdar.Ecdar; +import ecdar.presentations.StatePresentation; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; + +import java.math.BigDecimal; +import java.util.List; +import java.util.function.Consumer; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class Simulation { + public final String composition; + public final ObservableList simulatedComponents = FXCollections.observableArrayList(); + public final ObjectProperty initialState = new SimpleObjectProperty<>(); + public final ObservableMap variables = FXCollections.observableHashMap(); + public final ObservableMap clocks = FXCollections.observableHashMap(); + + public final ObjectProperty currentState = new SimpleObjectProperty<>(); + + public final ObservableList traceLog = FXCollections.observableArrayList(); + + public Simulation(String composition, State initialState) { + this.composition = composition; + setSimulatedComponents(composition); + + this.initialState.set(initialState); + this.currentState.set(initialState); + } + + private void setSimulatedComponents(String composition) { + // Match all referenced components by ignoring operators. + Pattern pattern = Pattern.compile("([\\w]*)", Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(composition); + + // Get all components referenced in the composition + List componentsToSimulateByName = matcher.results().map(MatchResult::group).collect(Collectors.toList()); + List componentsToSimulate = Ecdar.getProject().getComponents().stream() + .filter(component -> componentsToSimulateByName.contains(component.getName())) + .collect(Collectors.toList()); + + + simulatedComponents.addAll(componentsToSimulate); + } + + public void addStateToTraceLog(State state, Consumer onTraceLogStatePressed) { + StatePresentation statePresentation = new StatePresentation(state); + + statePresentation.setOnMouseReleased(event -> { + event.consume(); + onTraceLogStatePressed.accept(statePresentation); + }); + + traceLog.add(statePresentation); + } +} diff --git a/src/main/java/ecdar/abstractions/State.java b/src/main/java/ecdar/abstractions/State.java new file mode 100644 index 00000000..56c785e7 --- /dev/null +++ b/src/main/java/ecdar/abstractions/State.java @@ -0,0 +1,28 @@ +package ecdar.abstractions; + +import java.util.HashMap; +import java.util.List; + +public class State { + private final HashMap componentLocationMap; + private final List clockConstraints; + private final List decisions; + + public State(HashMap componentLocationMap, List clockConstraints, List decisions) { + this.componentLocationMap = componentLocationMap; + this.clockConstraints = clockConstraints; + this.decisions = decisions; + } + + public HashMap getComponentLocationMap() { + return componentLocationMap; + } + + public List getClockConstraints() { + return clockConstraints; + } + + public List getDecisions() { + return decisions; + } +} diff --git a/src/main/java/ecdar/abstractions/Transition.java b/src/main/java/ecdar/abstractions/Transition.java new file mode 100644 index 00000000..0db558d1 --- /dev/null +++ b/src/main/java/ecdar/abstractions/Transition.java @@ -0,0 +1,17 @@ +package ecdar.abstractions; + +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/backend/BackendHelper.java b/src/main/java/ecdar/backend/BackendHelper.java index f66dc546..a47c7718 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; @@ -83,35 +84,6 @@ public static void stopQueries() { Ecdar.getProject().getQueries().forEach(Query::cancel); } - /** - * 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 - * @return A reachability query string - */ - public static String getLocationReachableQuery(final Location location, final Component component) { - return component.getName() + "." + location.getId(); - } - - /** - * Generates a string for a deadlock query based on the component - * - * @param component The component which should be checked for deadlocks - * @return A deadlock query string - */ - public static String getExistDeadlockQuery(final Component component) { - // Get the names of the locations of this component. Used to produce the deadlock query - final String templateName = component.getName(); - final List locationNames = new ArrayList<>(); - - for (final Location location : component.getLocations()) { - locationNames.add(templateName + "." + location.getId()); - } - - return "(" + String.join(" || ", locationNames) + ") && deadlock"; - } - /** * Returns the Engine with the specified name, or null, if no such Engine exists * @@ -166,4 +138,15 @@ public static void setDefaultEngine(Engine newDefaultEngine) { public static void addEngineInstanceListener(Runnable runnable) { BackendHelper.enginesUpdatedListeners.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/Engine.java b/src/main/java/ecdar/backend/Engine.java index 61d039f3..96538f4f 100644 --- a/src/main/java/ecdar/backend/Engine.java +++ b/src/main/java/ecdar/backend/Engine.java @@ -1,14 +1,9 @@ package ecdar.backend; -import EcdarProtoBuf.ComponentProtos; -import EcdarProtoBuf.QueryProtos; import com.google.gson.JsonObject; -import com.google.protobuf.Empty; import ecdar.Ecdar; -import ecdar.abstractions.Component; -import ecdar.abstractions.Query; +import ecdar.abstractions.RequestSource; import ecdar.utility.serialize.Serializable; -import io.grpc.stub.StreamObserver; import javafx.beans.property.SimpleBooleanProperty; import java.util.*; @@ -23,16 +18,18 @@ public class Engine implements Serializable { private static final String PORT_RANGE_START = "portRangeStart"; private static final String PORT_RANGE_END = "portRangeEnd"; private static final String LOCKED = "locked"; - private final int responseDeadline = 20000; - private final int rerunRequestDelay = 200; - private final int numberOfRetriesPerQuery = 5; + private static final String IS_THREAD_SAFE = "isThreadSafe"; + + private static final int rerunRequestDelay = 200; + private static final int numberOfRetriesPerQuery = 5; private String name; private boolean isLocal; private boolean isDefault; + private boolean isThreadSafe; private int portStart; private int portEnd; - private SimpleBooleanProperty locked = new SimpleBooleanProperty(false); + private final SimpleBooleanProperty locked = new SimpleBooleanProperty(false); /** * This is either a path to the engines executable or an IP address at which the engine is running */ @@ -40,7 +37,6 @@ public class Engine implements Serializable { private final ArrayList startedConnections = new ArrayList<>(); private final BlockingQueue requestQueue = new ArrayBlockingQueue<>(200); // Magic number - // ToDo NIELS: Refactor to resize queue on port range change private final BlockingQueue availableConnections = new ArrayBlockingQueue<>(200); // Magic number private final EngineConnectionStarter connectionStarter = new EngineConnectionStarter(this); @@ -79,6 +75,14 @@ public void setDefault(boolean aDefault) { isDefault = aDefault; } + public boolean isThreadSafe() { + return isThreadSafe; + } + + public void setIsThreadSafe(boolean threadSafe) { + isThreadSafe = threadSafe; + } + public String getEngineLocation() { return engineLocation; } @@ -123,45 +127,20 @@ public SimpleBooleanProperty getLockedProperty() { return locked; } - public ArrayList getStartedConnections() { + protected ArrayList getStartedConnections() { return startedConnections; } /** - * Enqueue query for execution with consumers for success and error + * Enqueue request for execution based on request source with consumers for success and error * - * @param query the query to enqueue for execution - * @param successConsumer consumer for returned QueryResponse - * @param errorConsumer consumer for any throwable that might result from the execution + * @param requestSource RequestSource instance to construct the request from + * @param successConsumer for returned gRPC response + * @param errorConsumer for any throwable received from the engine */ - public void enqueueQuery(Query query, Consumer successConsumer, Consumer errorConsumer) { - GrpcRequest request = new GrpcRequest(engineConnection -> { - StreamObserver responseObserver = new StreamObserver<>() { - @Override - public void onNext(QueryProtos.QueryResponse value) { - successConsumer.accept(value); - } - - @Override - public void onError(Throwable t) { - errorConsumer.accept(t); - setConnectionAsAvailable(engineConnection); - } - - @Override - public void onCompleted() { - // Release engine connection - setConnectionAsAvailable(engineConnection); - } - }; - - var queryBuilder = QueryProtos.Query.newBuilder() - .setId(0) - .setQuery(query.getType().getQueryName() + ": " + query.getQuery()); - - engineConnection.getStub().withDeadlineAfter(responseDeadline, TimeUnit.MILLISECONDS) - .sendQuery(queryBuilder.build(), responseObserver); - }); + public void enqueueRequest(RequestSource requestSource, Consumer successConsumer, Consumer errorConsumer) { + GrpcRequestFactory factory = new GrpcRequestFactory(() -> {}, this::setConnectionAsAvailable); + GrpcRequest request = requestSource.accept(factory, successConsumer, errorConsumer); requestQueue.add(request); } @@ -171,19 +150,10 @@ public void onCompleted() { * * @param connection to make available */ - public void setConnectionAsAvailable(EngineConnection connection) { + private void setConnectionAsAvailable(EngineConnection connection) { if (!availableConnections.contains(connection)) availableConnections.add(connection); } - /** - * Clears all queued queries, stops all active engines, and closes all open engine connections - */ - public void clear() throws BackendException { - BackendHelper.stopQueries(); - requestQueue.clear(); - closeConnections(); - } - /** * Filters the list of open {@link EngineConnection}s to the specified {@link Engine} and returns the * first match or attempts to start a new connection if none is found. @@ -202,12 +172,17 @@ private EngineConnection getConnection() throws BackendException.NoAvailableEngi if (newConnection != null) { startedConnections.add(newConnection); - initializeConnection(newConnection); + setConnectionAsAvailable(newConnection); } } - // Blocks until a connection becomes available - connection = availableConnections.take(); + if (isThreadSafe){ + connection = availableConnections.peek(); + } + else{ + // Block until a connection becomes available + connection = availableConnections.take(); + } } catch (InterruptedException e) { throw new RuntimeException(e); } @@ -215,49 +190,13 @@ private EngineConnection getConnection() throws BackendException.NoAvailableEngi return connection; } - /** - * Executes the gRPC requests required to initialize the connection for query execution - * NOTE: This will be unnecessary with the SW5 changes for the ProtoBuf - */ - private void initializeConnection(EngineConnection connection) { - QueryProtos.ComponentsUpdateRequest.Builder componentsBuilder = QueryProtos.ComponentsUpdateRequest.newBuilder(); - for (Component c : Ecdar.getProject().getComponents()) { - componentsBuilder.addComponents(ComponentProtos.Component.newBuilder().setJson(c.serialize().toString()).build()); - } - - StreamObserver observer = new StreamObserver<>() { - @Override - public void onNext(Empty value) { - } - - @Override - public void onError(Throwable t) { - try { - connection.close(); - } catch (BackendException.gRpcChannelShutdownException | - BackendException.EngineProcessDestructionException e) { - Ecdar.showToast("An error occurred while trying to start new connection to: \"" + getName() + "\" and an exception was thrown while trying to remove gRPC channel and potential process"); - } - startedConnections.remove(connection); - } - - @Override - public void onCompleted() { - if (startedConnections.contains(connection)) setConnectionAsAvailable(connection); - } - }; - - connection.getStub().withDeadlineAfter(responseDeadline, TimeUnit.MILLISECONDS) - .updateComponents(componentsBuilder.build(), observer); - } - - /** + /** * Close all open engine connections and kill all locally running processes * * @throws BackendException if one or more connections throw an exception on {@link EngineConnection#close()} * (use getSuppressed() to see all thrown exceptions) */ - public void closeConnections() throws BackendException { + protected void closeConnections() throws BackendException { // Create a list for storing all terminated connection List> closeFutures = new ArrayList<>(); BackendException exceptions = new BackendException("Exceptions were thrown while attempting to close engine connections on " + getName()); @@ -329,6 +268,7 @@ public JsonObject serialize() { result.addProperty(NAME, getName()); result.addProperty(IS_LOCAL, isLocal()); result.addProperty(IS_DEFAULT, isDefault()); + result.addProperty(IS_THREAD_SAFE, isThreadSafe()); result.addProperty(LOCATION, getEngineLocation()); result.addProperty(PORT_RANGE_START, getPortStart()); result.addProperty(PORT_RANGE_END, getPortEnd()); @@ -342,6 +282,7 @@ public void deserialize(final JsonObject json) { setName(json.getAsJsonPrimitive(NAME).getAsString()); setLocal(json.getAsJsonPrimitive(IS_LOCAL).getAsBoolean()); setDefault(json.getAsJsonPrimitive(IS_DEFAULT).getAsBoolean()); + setIsThreadSafe(json.has(IS_THREAD_SAFE) && json.getAsJsonPrimitive(IS_THREAD_SAFE).getAsBoolean()); setEngineLocation(json.getAsJsonPrimitive(LOCATION).getAsString()); setPortStart(json.getAsJsonPrimitive(PORT_RANGE_START).getAsInt()); setPortEnd(json.getAsJsonPrimitive(PORT_RANGE_END).getAsInt()); diff --git a/src/main/java/ecdar/backend/GrpcRequestFactory.java b/src/main/java/ecdar/backend/GrpcRequestFactory.java new file mode 100644 index 00000000..e90dd78e --- /dev/null +++ b/src/main/java/ecdar/backend/GrpcRequestFactory.java @@ -0,0 +1,116 @@ +package ecdar.backend; + +import EcdarProtoBuf.ComponentProtos; +import EcdarProtoBuf.QueryProtos; +import ecdar.Ecdar; +import ecdar.abstractions.Component; +import ecdar.abstractions.Query; +import ecdar.abstractions.Decision; +import io.grpc.stub.StreamObserver; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public class GrpcRequestFactory { + private final Runnable onNext; + private final Consumer onFinished; + private static final int responseDeadline = 20000; + + public GrpcRequestFactory(Runnable onNext, Consumer onFinished) { + this.onNext = onNext; + this.onFinished = onFinished; + } + + public GrpcRequest create(Query query, Consumer queryResponseConsumer, Consumer errorConsumer) { + return new GrpcRequest(engineConnection -> { + StreamObserver responseObserver = getResponseStreamObserver(engineConnection, queryResponseConsumer, errorConsumer); + + var componentsInfoBuilder = BackendHelper.getComponentsInfoBuilder(query.getQuery()); + + var queryBuilder = QueryProtos.QueryRequest.newBuilder() + .setUserId(1) + .setQueryId(UUID.randomUUID().hashCode()) + .setSettings(QueryProtos.QueryRequest.Settings.newBuilder().setDisableClockReduction(true)) + .setQuery(query.getType().getQueryName() + ": " + query.getQuery()) + .setComponentsInfo(componentsInfoBuilder); + + engineConnection.getStub().withDeadlineAfter(responseDeadline, TimeUnit.MILLISECONDS) + .sendQuery(queryBuilder.build(), responseObserver); + }); + } + + public GrpcRequest create(Decision decision, Consumer simulationStepResponseConsumer, Consumer errorConsumer) { + if (decision.isInitial()) { + return createInitialSimulationStepRequest(decision, simulationStepResponseConsumer, errorConsumer); + } + + return createSimulationStepRequest(decision, simulationStepResponseConsumer, errorConsumer); + } + + private GrpcRequest createInitialSimulationStepRequest(Decision step, Consumer simulationStepResponseConsumer, Consumer errorConsumer) { + return new GrpcRequest(engineConnection -> { + StreamObserver responseObserver = getResponseStreamObserver(engineConnection, simulationStepResponseConsumer, errorConsumer); + ComponentProtos.ComponentsInfo.Builder comInfo = getComponentInfoBuilder(); + + var simStartRequest = QueryProtos.SimulationStartRequest.newBuilder(); + var simInfo = QueryProtos.SimulationInfo.newBuilder() + .setComponentComposition(step.composition) + .setComponentsInfo(comInfo); + simStartRequest.setSimulationInfo(simInfo); + engineConnection.getStub().withDeadlineAfter(responseDeadline, TimeUnit.MILLISECONDS) + .startSimulation(simStartRequest.build(), responseObserver); + }); + } + + private GrpcRequest createSimulationStepRequest(Decision decision, Consumer simulationStepResponseConsumer, Consumer errorConsumer) { + return new GrpcRequest(engineConnection -> { + StreamObserver responseObserver = getResponseStreamObserver(engineConnection, simulationStepResponseConsumer, errorConsumer); + + ComponentProtos.ComponentsInfo.Builder comInfo = getComponentInfoBuilder(); + + var simStepRequest = QueryProtos.SimulationStepRequest.newBuilder(); + var simInfo = QueryProtos.SimulationInfo.newBuilder() + .setComponentComposition(decision.composition) + .setComponentsInfo(comInfo); + simStepRequest.setSimulationInfo(simInfo); + + // ToDo NIELS: Handle below (might not be necessary) +// var specComp = ObjectProtos.ComponentInstance.newBuilder().setComponentName(decision.componentName).setComponentIndex(decision.componentId); + + simStepRequest.setChosenDecision(decision.protoDecision); + engineConnection.getStub().withDeadlineAfter(responseDeadline, TimeUnit.MILLISECONDS) + .takeSimulationStep(simStepRequest.build(), responseObserver); + }); + } + + private StreamObserver getResponseStreamObserver(EngineConnection connection, Consumer queryResponseConsumer, Consumer errorConsumer) { + return new StreamObserver<>() { + @Override + public void onNext(T value) { + onNext.run(); + queryResponseConsumer.accept(value); + } + + @Override + public void onError(Throwable t) { + onFinished.accept(connection); + errorConsumer.accept(t); + } + + @Override + public void onCompleted() { + onFinished.accept(connection); + } + }; + } + + private ComponentProtos.ComponentsInfo.Builder getComponentInfoBuilder() { + 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()); + return comInfo; + } +} diff --git a/src/main/java/ecdar/backend/StateFactory.java b/src/main/java/ecdar/backend/StateFactory.java new file mode 100644 index 00000000..9527f2e1 --- /dev/null +++ b/src/main/java/ecdar/backend/StateFactory.java @@ -0,0 +1,141 @@ +package ecdar.backend; + +import EcdarProtoBuf.ObjectProtos; +import EcdarProtoBuf.QueryProtos; +import ecdar.abstractions.ClockConstraint; +import ecdar.abstractions.Decision; +import ecdar.abstractions.State; + +import java.util.*; +import java.util.stream.Collectors; + +public class StateFactory { + /** + * Create a state instance from the composition and simulation step response + * + * @param composition of the client requesting the state + * @param response the simulation step response to generate the state from + * @return the generated state + */ + public State createState(String composition, QueryProtos.SimulationStepResponse response) { + ObjectProtos.State sourceState = response.getFullState(); + + HashMap componentLocationsMap = loadLocations(sourceState.getLocationTree()); + + // ToDo: Handle all conjunctions + List clockConstraints = loadClockConstraints(sourceState.getZone().getConjunctions(0).getConstraintsList()); + List decisions = loadDecisions(composition, response.getNewDecisionPointsList()); + + return new State(componentLocationsMap, clockConstraints, decisions); + } + + /** + * Create the state instance representing the initial state + * + * @param composition of the client requesting the state + * @param response the simulation step response to generate the state from + * @return the generated initial state + */ + public State createInitialState(String composition, QueryProtos.SimulationStepResponse response) { + ObjectProtos.State sourceState = response.getFullState(); + + HashMap componentLocationsMap = loadLocations(sourceState.getLocationTree()); + List decisions = loadDecisions(composition, response.getNewDecisionPointsList()); + + // ToDO: Clock constraints are currently not specified for the initial state. + // Should account for the initial locations' invariant. + List clockConstraints = new ArrayList<>(); + + return new State(componentLocationsMap, clockConstraints, decisions); + } + + private HashMap loadLocations(ObjectProtos.LocationTree rootLocationNode) { + HashMap componentLocationsMap = new HashMap<>(); + + Queue locationNodes = new ArrayDeque<>(); + locationNodes.add(rootLocationNode); + + ObjectProtos.LocationTree currentNode; + while ((currentNode = locationNodes.poll()) != null) { + switch (currentNode.getNodeTypeCase()) { + case LEAF_LOCATION: + componentLocationsMap.put( + currentNode.getLeafLocation().getComponentInstance().getComponentName(), + currentNode.getLeafLocation().getId()); + + case BINARY_LOCATION_OP: { + locationNodes.add(currentNode.getBinaryLocationOp().getLeft()); + locationNodes.add(currentNode.getBinaryLocationOp().getRight()); + } + + case SPECIAL_LOCATION: // ToDo: Implement visualization of inconsistent and universal locations + + case NODETYPE_NOT_SET: // Will never happen + } + } + + return componentLocationsMap; + } + + private List loadDecisions(String composition, List decisionPointsList) { + List decisions = new ArrayList<>(); + + for (ObjectProtos.Decision protoDecision : decisionPointsList) { + List edgeIds = protoDecision.getEdgesList().stream() + .map(ObjectProtos.Edge::getId) + .collect(Collectors.toList()); + + List clockConstraints = protoDecision.getSource().getZone().getConjunctions(0) + .getConstraintsList().stream() + .map(this::generateClockConstraint) + .collect(Collectors.toList()); + + decisions.add(new Decision( + composition, + edgeIds, + protoDecision.getAction(), + clockConstraints, + protoDecision)); + } + + return decisions; + } + + private List loadClockConstraints(List constraintsList) { + List clockConstraints = new ArrayList<>(); + + for (var constraint : constraintsList) { + clockConstraints.add(generateClockConstraint(constraint)); + } + + return clockConstraints; + } + + /** + * Generates a ClockConstraint object from a ProtoBuf constraint + * + * @param constraint the ProtoBuf constraint to generate from + * @return the generated ClockConstraint + */ + private ClockConstraint generateClockConstraint(ObjectProtos.Constraint constraint) { + var clock1 = constraint.getX().getComponentClock().getClockName(); + var clock2 = constraint.getY().getComponentClock().getClockName(); + var constant = constraint.getC(); + var strict = constraint.getStrict(); + + if (clock1.equals("")) { + // The first clock is the zero-clock + return new ClockConstraint(clock2, null, Math.abs(constant), '>', strict); + } else if (clock2.equals("")) { + // The second clock is the zero-clock + return new ClockConstraint(clock1, null, Math.abs(constant), '<', strict); + } else { + // None of the clocks are the zero-clock + if (constant >= 0) { + return new ClockConstraint(clock1, clock2, Math.abs(constant), '<', strict); + } else { + return new ClockConstraint(clock2, clock1, Math.abs(constant), '>', strict); + } + } + } +} diff --git a/src/main/java/ecdar/controllers/CanvasController.java b/src/main/java/ecdar/controllers/CanvasController.java index e72b705e..7427c70a 100644 --- a/src/main/java/ecdar/controllers/CanvasController.java +++ b/src/main/java/ecdar/controllers/CanvasController.java @@ -61,7 +61,7 @@ public HighLevelModelPresentation getActiveModelPresentation() { */ public void setActiveModelPresentation(final HighLevelModelPresentation model) { activeModelPresentation.set(model); - Platform.runLater(EcdarController.getActiveCanvasPresentation().getController()::leaveTextAreas); + Platform.runLater(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController()::leaveTextAreas); Platform.runLater(zoomHelper::zoomToFit); } diff --git a/src/main/java/ecdar/controllers/ComponentController.java b/src/main/java/ecdar/controllers/ComponentController.java index a9cce850..39702255 100644 --- a/src/main/java/ecdar/controllers/ComponentController.java +++ b/src/main/java/ecdar/controllers/ComponentController.java @@ -224,11 +224,45 @@ private void initializeSignature() { getComponent().getInputStrings().forEach((channel) -> insertSignatureArrow(channel, EdgeStatus.INPUT)); } - /*** + + /** * Initialize the listeners, that listen for changes in the input and output edges of the presented component. * The view is updated whenever an insert (deletions are also a type of insert) is reported */ private void initializeSignatureListeners() { + getComponent().getIsFailingProperty().addListener((observable, oldValue, newValue) -> { + if (getComponent().getIsFailing()) { + for (Node n : inputSignatureContainer.getChildren()) { + if (n instanceof SignatureArrow) { + for (String label : component.get().getFailingIOStrings()) { + if (Objects.equals(((SignatureArrow) n).getSignatureArrowLabel(), label)) { + ((SignatureArrow) n).recolorToRed(); + } + } + } + } + for (Node n : outputSignatureContainer.getChildren()) { + if (n instanceof SignatureArrow) { + for (String label : component.get().getFailingIOStrings()) { + if (Objects.equals(((SignatureArrow) n).getSignatureArrowLabel(), label)) { + ((SignatureArrow) n).recolorToRed(); + } + } + } + } + } else { + for (Node n : inputSignatureContainer.getChildren()) { + if (n instanceof SignatureArrow) { + ((SignatureArrow) n).recolorToGray(); + } + } + for (Node n : outputSignatureContainer.getChildren()) { + if (n instanceof SignatureArrow) { + ((SignatureArrow) n).recolorToGray(); + } + } + } + }); getComponent().getOutputStrings().addListener((ListChangeListener) c -> { // By clearing the container we don't have to fiddle with which elements are removed and added outputSignatureContainer.getChildren().clear(); @@ -245,6 +279,20 @@ private void initializeSignatureListeners() { }); } + /*** + * Finds the max height of the input/output signature container and the component + * @return a double of the largest height + */ + private double findMaxHeight() { + double inputHeight = inputSignatureContainer.getHeight(); + double outputHeight = outputSignatureContainer.getHeight(); + double componentHeight = component.get().getBox().getHeight(); + + double maxSignatureHeight = Math.max(outputHeight, inputHeight); + + return Math.max(maxSignatureHeight, componentHeight); + } + private void initializeNoIncomingEdgesWarning() { final Map messages = new HashMap<>(); @@ -505,7 +553,7 @@ private void initializeFinishEdgeContextMenu(final DisplayableEdge unfinishedEdg private void initializeLocationHandling() { final ListChangeListener locationListChangeListener = c -> { - if (c.next()) { + while (c.next()) { // Locations are added to the component c.getAddedSubList().forEach((loc) -> { // Check related to undo/redo stack @@ -548,7 +596,7 @@ private void initializeEdgeHandling() { // React on addition of edges to the component getComponent().getDisplayableEdges().addListener((ListChangeListener) c -> { - if (c.next()) { + while (c.next()) { // Edges are added to the component c.getAddedSubList().forEach(handleAddedEdge); @@ -746,7 +794,7 @@ public void moveAllNodesUp() { */ @FXML private void modelContainerPressed(final MouseEvent event) { - EcdarController.getActiveCanvasPresentation().getController().leaveTextAreas(); + Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().leaveTextAreas(); final DisplayableEdge unfinishedEdge = getComponent().getUnfinishedEdge(); dropdownCoordinatesInComponent = new Point2D(event.getX(), event.getY()); diff --git a/src/main/java/ecdar/controllers/DecisionController.java b/src/main/java/ecdar/controllers/DecisionController.java new file mode 100644 index 00000000..d08aeebf --- /dev/null +++ b/src/main/java/ecdar/controllers/DecisionController.java @@ -0,0 +1,39 @@ +package ecdar.controllers; + +import com.jfoenix.controls.JFXRippler; +import ecdar.abstractions.Decision; +import ecdar.utility.helpers.ClockConstraintsHandler; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.Initializable; +import javafx.scene.control.Label; + +import java.net.URL; +import java.util.ResourceBundle; + +public class DecisionController implements Initializable { + public JFXRippler rippler; + public Label action; + public Label clockConstraints; + + private final ObjectProperty decision = new SimpleObjectProperty<>(); + private final ClockConstraintsHandler clockConstraintsHandler = new ClockConstraintsHandler(); + + @Override + public void initialize(URL location, ResourceBundle resources) { + decision.addListener((observable, oldValue, newValue) -> { + action.setText(newValue.action); + clockConstraints.setText( + clockConstraintsHandler.getStateClockConstraintsString(newValue.clockConstraints) + ); + }); + } + + public Decision getDecision() { + return decision.get(); + } + + public void setDecision(Decision decision) { + this.decision.set(decision); + } +} diff --git a/src/main/java/ecdar/controllers/EcdarController.java b/src/main/java/ecdar/controllers/EcdarController.java index 9fe1d628..da0f7918 100644 --- a/src/main/java/ecdar/controllers/EcdarController.java +++ b/src/main/java/ecdar/controllers/EcdarController.java @@ -11,11 +11,11 @@ import ecdar.mutation.MutationTestPlanPresentation; import ecdar.mutation.models.MutationTestPlan; import ecdar.presentations.*; +import ecdar.presentations.SimulationInitializationDialogPresentation; +import ecdar.presentations.SimulationPresentation; import ecdar.utility.UndoRedoStack; import ecdar.utility.colors.Color; -import ecdar.utility.colors.EnabledColor; import ecdar.utility.helpers.SelectHelper; -import ecdar.utility.helpers.ZoomHelper; import ecdar.utility.keyboard.Keybind; import ecdar.utility.keyboard.KeyboardTracker; import ecdar.utility.keyboard.NudgeDirection; @@ -25,7 +25,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; @@ -41,7 +40,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,39 +47,19 @@ import java.io.IOException; import java.net.URL; 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 - public static boolean reachabilityServiceEnabled = false; - 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 ProjectPanePresentation projectPane; - public QueryPanePresentation queryPane; public Rectangle bottomFillerElement; - 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; @@ -92,10 +70,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; @@ -109,9 +83,9 @@ public class EcdarController implements Initializable { // The program top menu public MenuBar menuBar; - public MenuItem menuBarViewFilePanel; + public MenuItem menuBarViewProjectPanel; public MenuItem menuBarViewQueryPanel; - public MenuItem menuBarAutoscaling; + public MenuItem menuBarViewAutoscaling; public Menu menuViewMenuScaling; public ToggleGroup scaling; public RadioMenuItem scaleXS; @@ -122,6 +96,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; @@ -137,6 +113,7 @@ public class EcdarController implements Initializable { public MenuItem menuBarHelpAbout; public MenuItem menuBarHelpTest; + public JFXToggleButton switchGuiView; public Snackbar snackbar; public HBox statusBar; public Label statusLabel; @@ -150,6 +127,10 @@ public class EcdarController implements Initializable { public StackPane engineOptionsDialogContainer; public EngineOptionsDialogPresentation engineOptionsDialog; + + public StackPane simulationInitializationDialogContainer; + public SimulationInitializationDialogPresentation simulationInitializationDialog; + public final DoubleProperty scalingProperty = new SimpleDoubleProperty(); private static JFXDialog _queryDialog; @@ -157,16 +138,62 @@ public class EcdarController implements Initializable { private static Text _queryTextQuery; private static final Text temporaryComponentWatermark = new Text("Temporary component"); - public static void runReachabilityAnalysis() { - if (!reachabilityServiceEnabled) return; + /** + * Enumeration to keep track of which mode the application is in + */ + public enum Mode { + Editor, Simulator + } + + /** + * currentMode is a property that keeps track of which mode the application is in. + * The initial mode is Mode.Editor + */ + public static final ObjectProperty currentMode = new SimpleObjectProperty<>(Mode.Editor); + + private static final EditorPresentation editorPresentation = new EditorPresentation(); + private SimulationPresentation simulationPresentation; + + @Override + public void initialize(final URL location, final ResourceBundle resources) { + initializeDialogs(); + initializeKeybindings(); + initializeStatusBar(); + initializeMenuBar(); - reachabilityTime = System.currentTimeMillis() + 500; + enterEditorMode(); } - private static final ObjectProperty activeCanvasPresentation = new SimpleObjectProperty<>(new CanvasPresentation()); + public StackPane getCenter() { + if (currentMode.get().equals(Mode.Editor)) { + return editorPresentation.getController().canvasPane; + } else { + return simulationPresentation; + } + } public static EdgeStatus getGlobalEdgeStatus() { - return globalEdgeStatus.get(); + return editorPresentation.getController().getGlobalEdgeStatus(); + } + + public EditorPresentation getEditorPresentation() { + return editorPresentation; + } + + public StackPane getLeftModePane() { + if (currentMode.get().equals(Mode.Editor)) { + return editorPresentation.getController().getLeftPane(); + } else { + return simulationPresentation.getController().getLeftPane(); + } + } + + public StackPane getRightModePane() { + if (currentMode.get().equals(Mode.Editor)) { + return editorPresentation.getController().getRightPane(); + } else { + return simulationPresentation.getController().getRightPane(); + } } public static void setTemporaryComponentWatermarkVisibility(boolean visibility) { @@ -197,36 +224,18 @@ 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; @@ -241,13 +250,11 @@ private void initilizeDialogs() { engineOptionsDialog.getController().closeButton.setOnMouseClicked(event -> { engineOptionsDialog.getController().cancelEngineOptionsChanges(); - dialog.close(); engineOptionsDialog.close(); }); engineOptionsDialog.getController().saveButton.setOnMouseClicked(event -> { if (engineOptionsDialog.getController().saveChangesToEngineOptions()) { - dialog.close(); engineOptionsDialog.close(); } }); @@ -258,6 +265,18 @@ private void initilizeDialogs() { Engine defaultBackend = BackendHelper.getEngines().stream().filter(Engine::isDefault).findFirst().orElse(BackendHelper.getEngines().get(0)); BackendHelper.setDefaultEngine(defaultBackend); } + + initializeDialog(simulationInitializationDialog, simulationInitializationDialogContainer); + + simulationInitializationDialog.getController().cancelButton.setOnMouseClicked(event -> { + switchGuiView.setSelected(false); + simulationInitializationDialog.close(); + }); + + simulationInitializationDialog.getController().startButton.setOnMouseClicked(event -> { + currentMode.setValue(Mode.Simulator); + simulationInitializationDialog.close(); + }); } private void initializeDialog(JFXDialog dialog, StackPane dialogContainer) { @@ -273,10 +292,6 @@ private void initializeDialog(JFXDialog dialog, StackPane dialogContainer) { dialogContainer.setMouseTransparent(false); }); - projectPane.getStyleClass().add("responsive-pane-sizing"); - queryPane.getStyleClass().add("responsive-pane-sizing"); - - initializeEdgeStatusHandling(); initializeKeybindings(); initializeStatusBar(); } @@ -307,17 +322,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); @@ -331,133 +343,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, selectable.getColor()))); - 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)); - }, () -> { // Undo - previousColor.forEach(selectableEnabledColorPair -> selectableEnabledColorPair.getKey().color(selectableEnabledColorPair.getValue())); - }, 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() { - new Thread(() -> { - while (Ecdar.shouldRunBackgroundQueries.get()) { - // Wait for the reachability (the last time we changed the model) becomes smaller than the current time - while (reachabilityTime > System.currentTimeMillis()) { - try { - Thread.sleep(2000); - Debug.backgroundThreads.removeIf(thread -> !thread.isAlive()); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - // We are now performing the analysis. Do not do another analysis before another change is introduced - reachabilityTime = Long.MAX_VALUE; - - // Cancel any ongoing analysis - if (reachabilityService != null) { - reachabilityService.shutdownNow(); - } - - // Start new analysis - reachabilityService = Executors.newFixedThreadPool(10); - - while (Debug.backgroundThreads.size() > 0) { - final Thread thread = Debug.backgroundThreads.get(0); - thread.interrupt(); - Debug.removeThread(thread); - } - - // Stop thread if background queries have been toggled off - if (!Ecdar.shouldRunBackgroundQueries.get()) return; - - Ecdar.getProject().getQueries().forEach(query -> { - if (query.isPeriodic()) query.execute(); - }); - - // List of threads to start - List threads = new ArrayList<>(); - - // Submit all background reachability queries - Ecdar.getProject().getComponents().forEach(component -> { - // Check if we should consider this component - if (!component.isIncludeInPeriodicCheck()) { - component.getLocations().forEach(location -> location.setReachability(Location.Reachability.EXCLUDED)); - } else { - component.getLocations().forEach(location -> { - final String locationReachableQuery = BackendHelper.getLocationReachableQuery(location, component); - - Query reachabilityQuery = new Query(locationReachableQuery, "", QueryState.UNKNOWN); - reachabilityQuery.setType(QueryType.REACHABILITY); - - reachabilityQuery.execute(); - - final Thread verifyThread = new Thread(reachabilityQuery::execute); - - verifyThread.setName(locationReachableQuery + " (" + verifyThread.getName() + ")"); - Debug.addThread(verifyThread); - threads.add(verifyThread); - }); - } - }); - - threads.forEach((verifyThread) -> reachabilityService.submit(verifyThread::start)); - } - }).start(); } private void initializeStatusBar() { @@ -512,36 +397,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); - rebindZoomShortcutBindings(newActiveCanvasPresentation.getController().zoomHelper); - } - - /** - * Binds the zoom shortcuts to the provided zoom helper (consequently unbinds the previously bound zoom helper) - */ - private static void rebindZoomShortcutBindings(ZoomHelper zoomHelper) { - KeyboardTracker.registerKeybind(KeyboardTracker.ZOOM_IN, new Keybind(new KeyCodeCombination(KeyCode.PLUS, KeyCombination.SHORTCUT_DOWN), zoomHelper::zoomIn)); - KeyboardTracker.registerKeybind(KeyboardTracker.ZOOM_OUT, new Keybind(new KeyCodeCombination(KeyCode.MINUS, KeyCombination.SHORTCUT_DOWN), zoomHelper::zoomOut)); - KeyboardTracker.registerKeybind(KeyboardTracker.ZOOM_TO_FIT, new Keybind(new KeyCodeCombination(KeyCode.EQUALS, KeyCombination.SHORTCUT_DOWN), zoomHelper::zoomToFit)); - KeyboardTracker.registerKeybind(KeyboardTracker.RESET_ZOOM, new Keybind(new KeyCodeCombination(KeyCode.DIGIT0, KeyCombination.SHORTCUT_DOWN), zoomHelper::resetZoom)); - } - - public void setActiveModelPresentationForActiveCanvas(HighLevelModelPresentation newActiveModelPresentation) { - projectPane.getController().swapHighlightBetweenTwoModelFiles(EcdarController.getActiveCanvasPresentation().getController().getActiveModelPresentation(), newActiveModelPresentation); - EcdarController.getActiveCanvasPresentation().getController().setActiveModelPresentation(newActiveModelPresentation); - } - private void initializeHelpMenu() { menuBarHelpHelp.setOnAction(event -> Ecdar.showHelp()); @@ -558,7 +413,6 @@ private void initializeHelpMenu() { }); aboutAcceptButton.setOnAction(event -> aboutDialog.close()); aboutDialog.setOnDialogClosed(event -> aboutContainer.setVisible(false)); // hide container when dialog is fully closed - } /** @@ -579,10 +433,6 @@ private void initializeOptionsMenu() { menuBarOptionsBackgroundQueries.setOnAction(event -> { final BooleanProperty shouldRunBackgroundQueries = Ecdar.toggleRunBackgroundQueries(); Ecdar.preferences.putBoolean("run_background_queries", shouldRunBackgroundQueries.get()); - if (shouldRunBackgroundQueries.get()) { - // If background queries have been turned back on, start a new thread - startBackgroundQueriesThread(); - } }); Ecdar.shouldRunBackgroundQueries.setValue(Ecdar.preferences.getBoolean("run_background_queries", true)); @@ -592,7 +442,7 @@ private void initializeOptionsMenu() { private void initializeEditMenu() { menuEditMoveLeft.setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCombination.CONTROL_DOWN)); menuEditMoveLeft.setOnAction(event -> { - final HighLevelModelController activeModelController = getActiveCanvasPresentation().getController().getActiveModelPresentation().getController(); + final HighLevelModelController activeModelController = editorPresentation.getController().getActiveCanvasPresentation().getController().getActiveModelPresentation().getController(); if (activeModelController instanceof ComponentController) ((ComponentController) activeModelController).moveAllNodesLeft(); else Ecdar.showToast("This can only be performed on components."); @@ -600,7 +450,7 @@ private void initializeEditMenu() { menuEditMoveRight.setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCombination.CONTROL_DOWN)); menuEditMoveRight.setOnAction(event -> { - final HighLevelModelController activeModelController = getActiveCanvasPresentation().getController().getActiveModelPresentation().getController(); + final HighLevelModelController activeModelController = editorPresentation.getController().getActiveCanvasPresentation().getController().getActiveModelPresentation().getController(); if (activeModelController instanceof ComponentController) ((ComponentController) activeModelController).moveAllNodesRight(); else Ecdar.showToast("This can only be performed on components."); @@ -608,7 +458,7 @@ private void initializeEditMenu() { menuEditMoveUp.setAccelerator(new KeyCodeCombination(KeyCode.UP, KeyCombination.CONTROL_DOWN)); menuEditMoveUp.setOnAction(event -> { - final HighLevelModelController activeModelController = getActiveCanvasPresentation().getController().getActiveModelPresentation().getController(); + final HighLevelModelController activeModelController = editorPresentation.getController().getActiveCanvasPresentation().getController().getActiveModelPresentation().getController(); if (activeModelController instanceof ComponentController) ((ComponentController) activeModelController).moveAllNodesUp(); else Ecdar.showToast("This can only be performed on components."); @@ -616,7 +466,7 @@ private void initializeEditMenu() { menuEditMoveDown.setAccelerator(new KeyCodeCombination(KeyCode.DOWN, KeyCombination.CONTROL_DOWN)); menuEditMoveDown.setOnAction(event -> { - final HighLevelModelController activeModelController = getActiveCanvasPresentation().getController().getActiveModelPresentation().getController(); + final HighLevelModelController activeModelController = editorPresentation.getController().getActiveCanvasPresentation().getController().getActiveModelPresentation().getController(); if (activeModelController instanceof ComponentController) ((ComponentController) activeModelController).moveAllNodesDown(); else Ecdar.showToast("This can only be performed on components."); @@ -627,28 +477,28 @@ 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(); menuBarViewQueryPanel.getGraphic().opacityProperty().bind(new When(isOpen).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()))); @@ -660,12 +510,52 @@ private void initializeViewMenu() { Ecdar.isSplitProperty().addListener((observable, oldValue, newValue) -> { if (newValue) { - Platform.runLater(this::setCanvasModeToSplit); + Platform.runLater(editorPresentation.getController()::setCanvasModeToSplit); } else { - Platform.runLater(this::setCanvasModeToSingular); + Platform.runLater(editorPresentation.getController()::setCanvasModeToSingular); } }); + 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 (true) { // ToDo NIELS: Add check for whether the simulation is running + 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)); @@ -678,7 +568,6 @@ private void initializeViewMenu() { break; } } - scaling.selectToggle(scaleM); // Necessary to avoid project pane appearing off-screen scaling.selectToggle((RadioMenuItem) matchingToggle); }); @@ -690,12 +579,11 @@ private void updateScaling(double newScale) { // Text do not scale on the canvas to avoid ugly elements, // this zooms in on the component in order to get the "same font size" - EcdarController.getActiveCanvasPresentation().getController().zoomHelper.setZoomLevel(newCalculatedScale / 13); + editorPresentation.getController().getActiveCanvasPresentation().getController().zoomHelper.setZoomLevel(newCalculatedScale / 13); 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); @@ -777,7 +665,7 @@ private static void setProjectDirectory(String path) throws IOException { /** * Saves the project to the {@link Ecdar#projectDirectory} path. - * This include making directories, converting project files (components and queries) + * This includes making directories, converting project files (components and queries) * into Json formatted files. */ public void save() { @@ -890,7 +778,7 @@ private void initializeNewMutationTestObjectMenuItem() { UndoRedoStack.pushAndPerform(() -> { // Perform Ecdar.getProject().getTestPlans().add(newPlan); - getActiveCanvasPresentation().getController().setActiveModelPresentation(new MutationTestPlanPresentation(newPlan)); + editorPresentation.getController().setActiveModelPresentationForActiveCanvas(new MutationTestPlanPresentation(newPlan)); }, () -> { // Undo Ecdar.getProject().getTestPlans().remove(newPlan); }, "Created new mutation test plan", ""); @@ -911,6 +799,8 @@ private void createNewProject() { Ecdar.showToast("Unable to initialize new project directory"); } + editorPresentation.getController().setActiveModelPresentationForActiveCanvas(editorPresentation.getController().projectPane.getController().getComponentPresentations().get(0)); + UndoRedoStack.clear(); CodeAnalysis.enable(); @@ -923,12 +813,12 @@ private void initializeFileExportAsPng() { menuBarFileExportAsPng.setAccelerator(new KeyCodeCombination(KeyCode.L, KeyCombination.SHORTCUT_DOWN)); menuBarFileExportAsPng.setOnAction(event -> { // If there is no active component or system - if (!(getActiveCanvasPresentation().getController().getActiveModelPresentation() instanceof ComponentPresentation || getActiveCanvasPresentation().getController().getActiveModelPresentation() instanceof SystemPresentation)) { + if (!(editorPresentation.getController().getActiveCanvasPresentation().getController().getActiveModelPresentation() instanceof ComponentPresentation || editorPresentation.getController().getActiveCanvasPresentation().getController().getActiveModelPresentation() instanceof SystemPresentation)) { Ecdar.showToast("No component or system to export."); return; } - CanvasPresentation canvas = getActiveCanvasPresentation(); + CanvasPresentation canvas = editorPresentation.getController().getActiveCanvasPresentation(); // If there was an active component, hide button for toggling declarations final ComponentPresentation presentation = canvas.getController().getActiveComponentPresentation(); @@ -947,7 +837,7 @@ private void initializeFileExportAsPng() { }); menuBarFileExportAsPngNoBorder.setOnAction(event -> { - CanvasPresentation canvas = getActiveCanvasPresentation(); + CanvasPresentation canvas = editorPresentation.getController().getActiveCanvasPresentation(); final ComponentPresentation presentation = canvas.getController().getActiveComponentPresentation(); @@ -965,176 +855,33 @@ 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. + * Changes the view and mode to the editor + * Only enter if the mode is not already Editor */ - private void setCanvasModeToSingular() { - canvasPane.getChildren().clear(); - CanvasPresentation canvasPresentation = new CanvasPresentation(); - if (activeCanvasPresentation.get().getController().getActiveModelPresentation() != null) { - canvasPresentation.getController().setActiveModelPresentation(activeCanvasPresentation.get().getController().getActiveModelPresentation()); - } - - canvasPane.getChildren().add(canvasPresentation); - setActiveCanvasPresentation(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()); - - projectPane.getController().setHighlightedForModelFiles(getActiveModelPresentations()); - - menuBarViewCanvasSplit.setText("Split canvas"); + private void enterEditorMode() { + borderPane.setCenter(editorPresentation); + updateSidePanes(editorPresentation.getController()); } /** - * 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 simulator + * Only enter if the mode is not already Simulator */ - 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 = projectPane.getController().getComponentPresentations(); - int currentCompNum = 0, numComponents = components.size(); - - // Add the canvasPresentation at the top-left - CanvasPresentation canvasPresentation = initializeNewCanvasPresentation(); - canvasPresentation.getController().setActiveModelPresentation(getActiveCanvasPresentation().getController().getActiveModelPresentation()); - 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().getActiveModelPresentation() != null && canvasPresentation.getController().getActiveModelPresentation().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().getActiveModelPresentation() != null && canvasPresentation.getController().getActiveModelPresentation().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); + private void enterSimulatorMode() { + simulationPresentation = new SimulationPresentation(simulationInitializationDialog.getController().simulationComboBox.getSelectionModel().getSelectedItem()); - canvasPane.getChildren().add(canvasGrid); - projectPane.getController().setHighlightedForModelFiles(getActiveModelPresentations()); - - menuBarViewCanvasSplit.setText("Merge canvases"); - } - - /** - * Get list of shown HighLevelModelPresentations - * - * @return a list of all the HighLevelModelPresentations currently being shown - */ - private List getActiveModelPresentations() { - Pane canvasChild = (Pane) canvasPane.getChildren().get(0); - - if (canvasChild instanceof GridPane) { - return canvasChild.getChildren() - .stream().map(canvas -> ((CanvasPresentation) canvas) - .getController().getActiveModelPresentation()).filter(Objects::nonNull).collect(Collectors.toList()); - } else if (canvasChild instanceof CanvasPresentation) { - return new ArrayList<>() { - { - ((CanvasPresentation) canvasChild).getController().getActiveModelPresentation(); - } - }; - } - - return new ArrayList<>(); + borderPane.setCenter(simulationPresentation); + updateSidePanes(simulationPresentation.getController()); } - /** - * 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().setActiveModelPresentation(null); - for (int currentCompNum = startIndex; currentCompNum < numComponents; currentCompNum++) { - if (getActiveCanvasPresentation().getController().getActiveModelPresentation() != components.get(currentCompNum)) { - canvasPresentation.getController().setActiveModelPresentation(components.get(currentCompNum)); - break; - } - } + private void updateSidePanes(ModeController modeController) { + leftPane.getChildren().clear(); + leftPane.getChildren().add(modeController.getLeftPane()); + rightPane.getChildren().clear(); + rightPane.getChildren().add(modeController.getRightPane()); - 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; } /** @@ -1172,7 +919,7 @@ private WritableImage scaleAndTakeSnapshot(CanvasPresentation canvas) { * @param image the image */ private void CropAndExportImage(final WritableImage image) { - final String name = getActiveCanvasPresentation().getController().getActiveModelPresentation().getController().getModel().getName(); + final String name = editorPresentation.getController().getActiveCanvasPresentation().getController().getActiveModelPresentation().getController().getModel().getName(); final FileChooser filePicker = new FileChooser(); filePicker.setTitle("Export png"); @@ -1294,21 +1041,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()) { - projectPane.showBottomInset(false); - queryPane.showBottomInset(false); - getActiveCanvasPresentation().getController().updateOffset(false); - } else { - projectPane.showBottomInset(true); - queryPane.showBottomInset(true); - getActiveCanvasPresentation().getController().updateOffset(true); - } - } - private void nudgeSelected(final NudgeDirection direction) { final List selectedElements = SelectHelper.getSelectedElements(); @@ -1341,88 +1073,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 e3d54d8b..16bb0e63 100644 --- a/src/main/java/ecdar/controllers/EdgeController.java +++ b/src/main/java/ecdar/controllers/EdgeController.java @@ -7,7 +7,6 @@ import ecdar.presentations.*; import ecdar.utility.Highlightable; import ecdar.utility.UndoRedoStack; -import ecdar.utility.colors.Color; import ecdar.utility.colors.EnabledColor; import ecdar.utility.helpers.*; import ecdar.utility.keyboard.Keybind; @@ -66,14 +65,13 @@ public void initialize(final URL location, final ResourceBundle resources) { // 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()) { + if (edge.get().isHighlighted()) { this.highlight(); } else { this.unhighlight(); } }); }); - initializeLinksListener(); ensureNailsInFront(); initializeSelectListener(); @@ -444,11 +442,11 @@ public void onChanged(final Change c) { dropDownMenu.hide(); }); - dropDownMenu.show(JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX() * EcdarController.getActiveCanvasZoomFactor().get(), event.getY() * EcdarController.getActiveCanvasZoomFactor().get()); + dropDownMenu.show(JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX() * Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get(), event.getY() * Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get()); } else if ((event.isShiftDown() && event.isPrimaryButtonDown()) || event.isMiddleButtonDown()) { - final double nailX = EcdarController.getActiveCanvasPresentation().mouseTracker.xProperty().subtract(getComponent().getBox().getXProperty()).doubleValue(); - final double nailY = EcdarController.getActiveCanvasPresentation().mouseTracker.yProperty().subtract(getComponent().getBox().getYProperty()).doubleValue(); + final double nailX = Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().mouseTracker.xProperty().subtract(getComponent().getBox().getXProperty()).doubleValue(); + final double nailY = Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().mouseTracker.yProperty().subtract(getComponent().getBox().getYProperty()).doubleValue(); final Nail newNail = new Nail(nailX, nailY); @@ -595,8 +593,8 @@ private void addEdgePropertyRow(final DropDownMenu dropDownMenu, final String ro } private Nail getNewNailBasedOnDropdownPosition() { - final double nailX = DropDownMenu.x / EcdarController.getActiveCanvasZoomFactor().get(); - final double nailY = DropDownMenu.y / EcdarController.getActiveCanvasZoomFactor().get(); + final double nailX = DropDownMenu.x / Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get(); + final double nailY = DropDownMenu.y / Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get(); return new Nail(nailX, nailY); } diff --git a/src/main/java/ecdar/controllers/EditorController.java b/src/main/java/ecdar/controllers/EditorController.java new file mode 100644 index 00000000..cf12107d --- /dev/null +++ b/src/main/java/ecdar/controllers/EditorController.java @@ -0,0 +1,426 @@ +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.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; +import javafx.application.Platform; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleDoubleProperty; +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.Collections; +import java.util.List; +import java.util.ResourceBundle; + +public class EditorController implements ModeController, 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; + + public final ProjectPanePresentation projectPane = new ProjectPanePresentation(); + public final QueryPanePresentation queryPane = new QueryPanePresentation(); + + 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(); + Platform.runLater(this::initializeSidePanes); + + // Update file coloring when active model changes + getActiveCanvasPresentation().getController().activeModelProperty().addListener((observable, oldModel, newModel) -> projectPane.getController().swapHighlightBetweenTwoModelFiles(oldModel, newModel)); + } + + private void initializeSidePanes() { + final DoubleProperty prevX = new SimpleDoubleProperty(); + final DoubleProperty prevWidth = new SimpleDoubleProperty(); + + queryPane.getController().resizeAnchor.setOnMousePressed(event -> { + event.consume(); + + prevX.set(event.getScreenX()); + prevWidth.set(queryPane.getWidth()); + }); + + queryPane.getController().resizeAnchor.setOnMouseDragged(event -> { + double diff = prevX.get() - event.getScreenX(); + + // Set bounds for resizing to be between 280px and half the screen width + final double newWidth = Math.min(Math.max(prevWidth.get() + diff, 280), Ecdar.getPresentation().getWidth() / 2); + + queryPane.setMinWidth(newWidth); + queryPane.setMaxWidth(newWidth); + }); + + projectPane.getStyleClass().add("responsive-pane-sizing"); + queryPane.getStyleClass().add("responsive-pane-sizing"); + } + + 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 setActiveModelPresentationForActiveCanvas(HighLevelModelPresentation newActiveModelPresentation) { + getActiveCanvasPresentation().getController().setActiveModelPresentation(newActiveModelPresentation); + + // Change zoom level to fit new active model + Platform.runLater(() -> getActiveCanvasPresentation().getController().zoomHelper.zoomToFit()); + } + + public DoubleProperty getActiveCanvasZoomFactor() { + return getActiveCanvasPresentation().getController().zoomHelper.currentZoomFactor; + } + + 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, selectable.getColor())); + }); + 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)); + }, () -> { // Undo + previousColor.forEach(selectableEnabledColorPair -> selectableEnabledColorPair.getKey().color(selectableEnabledColorPair.getValue())); + }, 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(); + if (activeCanvasPresentation.get().getController().getActiveModelPresentation() != null) { + canvasPresentation.getController().setActiveModelPresentation(activeCanvasPresentation.get().getController().getActiveModelPresentation()); + } + + canvasPane.getChildren().add(canvasPresentation); + setActiveCanvasPresentation(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()); + + Platform.runLater(() -> projectPane.getController().setHighlightedForModelFiles(Collections.singletonList(getActiveCanvasPresentation().getController().getActiveModelPresentation()))); + } + + /** + * 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 = projectPane.getController().getComponentPresentations(); + int currentCompNum = 0, numComponents = components.size(); + + // Add the canvasPresentation at the top-left + CanvasPresentation canvasPresentation = initializeNewCanvasPresentation(); + canvasPresentation.getController().setActiveModelPresentation(getActiveCanvasPresentation().getController().getActiveModelPresentation()); + 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().getActiveModelPresentation() != null && canvasPresentation.getController().getActiveModelPresentation().getController().getModel().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().getActiveModelPresentation() != null && canvasPresentation.getController().getActiveModelPresentation().getController().getModel().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 componentPresentations the list of componentPresentations 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 componentPresentations, int startIndex) { + CanvasPresentation canvasPresentation = initializeNewCanvasPresentation(); + + int numComponents = componentPresentations.size(); + canvasPresentation.getController().setActiveModelPresentation(null); + for (int currentCompNum = startIndex; currentCompNum < numComponents; currentCompNum++) { + if (getActiveCanvasPresentation().getController().getActiveModelPresentation() != componentPresentations.get(currentCompNum)) { + canvasPresentation.getController().setActiveModelPresentation(componentPresentations.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(); + } + + @Override + public StackPane getLeftPane() { + return projectPane; + } + + @Override + public StackPane getRightPane() { + return queryPane; + } +} diff --git a/src/main/java/ecdar/controllers/EngineInstanceController.java b/src/main/java/ecdar/controllers/EngineInstanceController.java index ccf26c51..8daf2668 100644 --- a/src/main/java/ecdar/controllers/EngineInstanceController.java +++ b/src/main/java/ecdar/controllers/EngineInstanceController.java @@ -52,6 +52,7 @@ public class EngineInstanceController implements Initializable { public JFXTextField portRangeStart; public JFXTextField portRangeEnd; public RadioButton defaultEngineRadioButton; + public JFXCheckBox threadSafeEngineCheckBox; @Override public void initialize(URL location, ResourceBundle resources) { @@ -93,6 +94,7 @@ public void setEngine(Engine instance) { this.engineName.setText(instance.getName()); this.isLocal.setSelected(instance.isLocal()); this.defaultEngineRadioButton.setSelected(instance.isDefault()); + this.threadSafeEngineCheckBox.setSelected(instance.isThreadSafe()); // Check if the path or the address should be used if (isLocal.isSelected()) { @@ -113,6 +115,7 @@ public Engine updateEngineInstance() { engine.setName(engineName.getText()); engine.setLocal(isLocal.isSelected()); engine.setDefault(defaultEngineRadioButton.isSelected()); + engine.setIsThreadSafe(threadSafeEngineCheckBox.isSelected()); engine.setEngineLocation(isLocal.isSelected() ? pathToEngine.getText() : address.getText()); engine.setPortStart(Integer.parseInt(portRangeStart.getText())); engine.setPortEnd(Integer.parseInt(portRangeEnd.getText())); diff --git a/src/main/java/ecdar/controllers/EngineOptionsDialogController.java b/src/main/java/ecdar/controllers/EngineOptionsDialogController.java index fafd6592..285096a3 100644 --- a/src/main/java/ecdar/controllers/EngineOptionsDialogController.java +++ b/src/main/java/ecdar/controllers/EngineOptionsDialogController.java @@ -90,9 +90,6 @@ public boolean saveChangesToEngineOptions() { Engine defaultEngine = engines.stream().filter(Engine::isDefault).findFirst().orElse(engines.get(0)); BackendHelper.setDefaultEngine(defaultEngine); - String defaultEngineName = (defaultEngine.getName()); - Ecdar.preferences.put("default_engine", defaultEngineName); - return true; } else { return false; @@ -100,7 +97,7 @@ public boolean saveChangesToEngineOptions() { } /** - * Resets the engines to the default engines present in the 'default_engines.json' file. + * Resets the engines to those packaged with the system. */ public void resetEnginesToDefault() { updateEnginesInGUI(getPackagedEngines()); @@ -182,9 +179,10 @@ private ArrayList getPackagedEngines() { reveaal.setName("Reveaal"); reveaal.setLocal(true); reveaal.setDefault(true); - reveaal.setPortStart(5032); - reveaal.setPortEnd(5040); + reveaal.setPortStart(5040); + reveaal.setPortEnd(5042); reveaal.lockInstance(); + reveaal.setIsThreadSafe(true); // Load correct Reveaal executable based on OS List potentialFilesForReveaal = new ArrayList<>(); @@ -203,6 +201,7 @@ private ArrayList getPackagedEngines() { jEcdar.setPortStart(5042); jEcdar.setPortEnd(5050); jEcdar.lockInstance(); + jEcdar.setIsThreadSafe(false); // Load correct j-Ecdar executable based on OS List potentialFiledForJEcdar = new ArrayList<>(); @@ -255,7 +254,6 @@ private boolean setEnginePathIfFileExists(Engine engine, List potentialF * @param newEnginePresentation The presentation of the new engine instance */ private void addEnginePresentationToList(EnginePresentation newEnginePresentation) { - newEnginePresentation.getController().defaultEngineRadioButton.setSelected(engineInstanceList.getChildren().isEmpty()); engineInstanceList.getChildren().add(newEnginePresentation); newEnginePresentation.getController().moveEngineInstanceUpRippler.setOnMouseClicked((mouseEvent) -> moveEngineInstance(newEnginePresentation, -1)); newEnginePresentation.getController().moveEngineInstanceDownRippler.setOnMouseClicked((mouseEvent) -> moveEngineInstance(newEnginePresentation, +1)); diff --git a/src/main/java/ecdar/controllers/LeftSimPaneController.java b/src/main/java/ecdar/controllers/LeftSimPaneController.java new file mode 100755 index 00000000..2461cd52 --- /dev/null +++ b/src/main/java/ecdar/controllers/LeftSimPaneController.java @@ -0,0 +1,49 @@ +package ecdar.controllers; + +import ecdar.presentations.StatePresentation; +import ecdar.presentations.TracePanePresentation; +import ecdar.utility.colors.Color; +import javafx.collections.ObservableList; +import javafx.fxml.Initializable; +import javafx.geometry.Insets; +import javafx.scene.layout.*; + +import java.net.URL; +import java.util.ResourceBundle; + +public class LeftSimPaneController implements Initializable { + public StackPane root; + public VBox content; + + public TracePanePresentation tracePanePresentation; + + @Override + public void initialize(URL location, ResourceBundle resources) { + initializeBackground(); + initializeRightBorder(); + } + + private void initializeBackground() { + content.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() { + tracePanePresentation.getController().toolbar.setBorder(new Border(new BorderStroke( + Color.GREY_BLUE.getColor(Color.Intensity.I900), + BorderStrokeStyle.SOLID, + CornerRadii.EMPTY, + new BorderWidths(0, 1, 0, 0) + ))); + } + + public void setTraceLog(ObservableList traceLog) { + tracePanePresentation.getController().setTraceLog(traceLog); + } +} diff --git a/src/main/java/ecdar/controllers/LocationController.java b/src/main/java/ecdar/controllers/LocationController.java index 0aadeaeb..d6392b4f 100644 --- a/src/main/java/ecdar/controllers/LocationController.java +++ b/src/main/java/ecdar/controllers/LocationController.java @@ -192,7 +192,7 @@ public void initializeDropDownMenu() { dropDownMenu.addClickableListElement("Is " + getLocation().getId() + " reachable?", event -> { dropDownMenu.hide(); // Generate the query from the engine - final String reachabilityQuery = BackendHelper.getLocationReachableQuery(getLocation(), getComponent()); + final String reachabilityQuery = getComponent().getName() + "." + getLocation().getId(); // Add proper comment final String reachabilityComment = "Is " + getLocation().getMostDescriptiveIdentifier() + " reachable?"; diff --git a/src/main/java/ecdar/controllers/MessageCollectionController.java b/src/main/java/ecdar/controllers/MessageCollectionController.java index 4d2857b1..c8269bc2 100644 --- a/src/main/java/ecdar/controllers/MessageCollectionController.java +++ b/src/main/java/ecdar/controllers/MessageCollectionController.java @@ -24,7 +24,6 @@ import java.util.List; import java.util.Map; import java.util.ResourceBundle; -import java.util.function.BiConsumer; import java.util.function.Consumer; public class MessageCollectionController implements Initializable { @@ -93,8 +92,8 @@ private void initializeHeadline(final Component component) { headline.setStyle("-fx-underline: false;"); }; - final EventHandler onMousePressed = event -> Ecdar.getPresentation().getController().setActiveModelPresentationForActiveCanvas( - Ecdar.getPresentation().getController(). + final EventHandler onMousePressed = event -> Ecdar.getPresentation().getController().getEditorPresentation().getController().setActiveModelPresentationForActiveCanvas( + Ecdar.getPresentation().getController().getEditorPresentation().getController(). projectPane.getController().getComponentPresentations(). stream().filter(componentPresentation -> componentPresentation.getController().getComponent().equals(component)) .findFirst().orElse(null)); 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/ModeController.java b/src/main/java/ecdar/controllers/ModeController.java new file mode 100644 index 00000000..54be9495 --- /dev/null +++ b/src/main/java/ecdar/controllers/ModeController.java @@ -0,0 +1,8 @@ +package ecdar.controllers; + +import javafx.scene.layout.StackPane; + +public interface ModeController { + StackPane getLeftPane(); + StackPane getRightPane(); +} diff --git a/src/main/java/ecdar/controllers/ModelController.java b/src/main/java/ecdar/controllers/ModelController.java index e3369933..c56b2ba2 100644 --- a/src/main/java/ecdar/controllers/ModelController.java +++ b/src/main/java/ecdar/controllers/ModelController.java @@ -5,9 +5,9 @@ import ecdar.abstractions.HighLevelModel; import com.jfoenix.controls.JFXTextField; import ecdar.utility.UndoRedoStack; -import ecdar.utility.colors.Color; import ecdar.utility.colors.EnabledColor; import ecdar.utility.helpers.StringValidator; +import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -107,9 +107,11 @@ private void initializeName() { model.colorProperty().addListener(observable -> updateColor.run()); updateColor.run(); - // Center the text vertically and aff a left padding of CORNER_SIZE - name.setPadding(new Insets(2, 0, 0, CORNER_SIZE)); - name.setOnKeyPressed(EcdarController.getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler()); + Platform.runLater(() -> { + // Center the text vertically and add a left padding of CORNER_SIZE + name.setPadding(new Insets(2, 0, 0, CORNER_SIZE)); + name.setOnKeyPressed(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler()); + }); } /** @@ -272,11 +274,11 @@ private void initializesCornerDragAnchor(final Box box) { }); cornerAnchor.setOnMouseDragged(event -> { - double xDiff = (event.getScreenX() - prevX.get()) / EcdarController.getActiveCanvasZoomFactor().get(); // ToDo NIELS: Fix dependency + double xDiff = (event.getScreenX() - prevX.get()); final double newWidth = Math.max(prevWidth.get() + xDiff, getDragAnchorMinWidth()); box.setWidth(newWidth); - double yDiff = (event.getScreenY() - prevY.get()) / EcdarController.getActiveCanvasZoomFactor().get(); + double yDiff = (event.getScreenY() - prevY.get()); final double newHeight = Math.max(prevHeight.get() + yDiff, getDragAnchorMinHeight()); box.setHeight(newHeight); diff --git a/src/main/java/ecdar/controllers/ProcessController.java b/src/main/java/ecdar/controllers/ProcessController.java new file mode 100755 index 00000000..49d7d124 --- /dev/null +++ b/src/main/java/ecdar/controllers/ProcessController.java @@ -0,0 +1,309 @@ +package ecdar.controllers; + +import com.jfoenix.controls.JFXRippler; +import ecdar.Ecdar; +import ecdar.abstractions.*; +import ecdar.presentations.SimEdgePresentation; +import ecdar.presentations.SimLocationPresentation; +import ecdar.utility.helpers.UPPAALSyntaxHighlighter; +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.fxmisc.richtext.LineNumberFactory; +import org.fxmisc.richtext.StyleClassedTextArea; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.math.BigDecimal; +import java.net.URL; +import java.util.*; + +/** + * The controller for a process shown in the {@link SimulationController} + */ +public class ProcessController extends ModelController implements Initializable { + public StackPane componentPane; + public Pane modelContainerEdge; + public Pane modelContainerLocation; + public JFXRippler toggleValuesButton; + public VBox valueArea; + public StyleClassedTextArea declarationTextArea; + 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()); + // add line numbers to the declaration text area + declarationTextArea.setParagraphGraphicFactory(LineNumberFactory.get(declarationTextArea)); + 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); + }); + + declarationTextArea.appendText(component.getDeclarationsText()); + declarationTextArea.setStyleSpans(0, UPPAALSyntaxHighlighter.computeHighlighting(getComponent().getDeclarationsText())); + declarationTextArea.getStyleClass().add("component-declaration"); + } + + 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 HighLevelModel getModel() { + return component.get(); + } + + public ObservableMap getVariables() { + return variables; + } + + public ObservableMap getClocks() { + return clocks; + } + + /** + * Gets the minimum possible width when dragging the anchor. + * The width is based on the x coordinate of locations, nails and the signature arrows. + * @return the minimum possible width. + */ + @Override + double getDragAnchorMinWidth() { + final Component component = getComponent(); + double minWidth = Ecdar.CANVAS_PADDING * 10; + + for (final Location location : component.getLocations()) { + minWidth = Math.max(minWidth, location.getX() + Ecdar.CANVAS_PADDING * 2); + } + + for (final Edge edge : component.getEdges()) { + for (final Nail nail : edge.getNails()) { + minWidth = Math.max(minWidth, nail.getX() + Ecdar.CANVAS_PADDING); + } + } + 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. + * @return the minimum possible height. + */ + @Override + double getDragAnchorMinHeight() { + final Component component = getComponent(); + double minHeight = Ecdar.CANVAS_PADDING * 10; + + for (final Location location : component.getLocations()) { + minHeight = Math.max(minHeight, location.getY() + Ecdar.CANVAS_PADDING * 2); + } + + for (final Edge edge : component.getEdges()) { + for (final Nail nail : edge.getNails()) { + minHeight = Math.max(minHeight, nail.getY() + Ecdar.CANVAS_PADDING); + } + } + + return minHeight; + } +} diff --git a/src/main/java/ecdar/controllers/ProjectPaneController.java b/src/main/java/ecdar/controllers/ProjectPaneController.java index 5f53eb98..4295543e 100644 --- a/src/main/java/ecdar/controllers/ProjectPaneController.java +++ b/src/main/java/ecdar/controllers/ProjectPaneController.java @@ -70,7 +70,7 @@ public void initialize(final URL location, final ResourceBundle resources) { final FilePresentation globalDclPresentation = new FilePresentation(project.getGlobalDeclarations()); modelPresentationMap.put(globalDeclarationsPresentation, globalDclPresentation); globalDclPresentation.setOnMousePressed(event -> { - Ecdar.getPresentation().getController().setActiveModelPresentationForActiveCanvas(globalDeclarationsPresentation); + Ecdar.getPresentation().getController().getEditorPresentation().getController().setActiveModelPresentationForActiveCanvas(globalDeclarationsPresentation); }); filesList.getChildren().add(globalDclPresentation); @@ -84,7 +84,7 @@ public void initialize(final URL location, final ResourceBundle resources) { Platform.runLater(() -> { final var initializedModelPresentation = modelPresentationMap.keySet().stream().filter(mp -> mp instanceof ComponentPresentation).findFirst().orElse(null); - Ecdar.getPresentation().getController().setActiveModelPresentationForActiveCanvas(initializedModelPresentation); + Ecdar.getPresentation().getController().getEditorPresentation().getController().setActiveModelPresentationForActiveCanvas(initializedModelPresentation); }); } @@ -317,7 +317,7 @@ private void initializeCreateComponentKeybinding() { project.getComponents().remove(newComponent); }, "Created new component: " + newComponent.getName(), "add-circle"); - EcdarController.getActiveCanvasPresentation().getController().setActiveModelPresentation(getComponentPresentations().stream().filter(componentPresentation -> componentPresentation.getController().getComponent().equals(newComponent)).findFirst().orElse(null)); + Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().setActiveModelPresentation(getComponentPresentations().stream().filter(componentPresentation -> componentPresentation.getController().getComponent().equals(newComponent)).findFirst().orElse(null)); }); KeyboardTracker.registerKeybind(KeyboardTracker.CREATE_COMPONENT, binding); } @@ -333,19 +333,19 @@ private void handleAddedModelPresentation(final HighLevelModelPresentation model filesList.getChildren().add(filePresentation); } - modelPresentationMap.put(modelPresentation, filePresentation);// ToDo NIELS: Bind these two + modelPresentationMap.put(modelPresentation, filePresentation); if (modelPresentation instanceof ComponentPresentation) { componentPresentations.add((ComponentPresentation) modelPresentation); } // Open the component if the file is pressed filePresentation.setOnMousePressed(event -> { - final var previouslyActiveFile = modelPresentationMap.get(EcdarController.getActiveCanvasPresentation() + final var previouslyActiveFile = modelPresentationMap.get(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation() .getController() .getActiveModelPresentation()); if (previouslyActiveFile != null) previouslyActiveFile.getController().setIsActive(false); - Ecdar.getPresentation().getController().setActiveModelPresentationForActiveCanvas(modelPresentation); + Ecdar.getPresentation().getController().getEditorPresentation().getController().setActiveModelPresentationForActiveCanvas(modelPresentation); Platform.runLater(() -> { filePresentation.getController().setIsActive(true); }); @@ -353,20 +353,20 @@ private void handleAddedModelPresentation(final HighLevelModelPresentation model modelPresentation.getController().getModel().nameProperty().addListener(obs -> sortPresentations()); filePresentation.getController().setIsActive(true); - Platform.runLater(() -> Ecdar.getPresentation().getController().setActiveModelPresentationForActiveCanvas(modelPresentation)); + Platform.runLater(() -> Ecdar.getPresentation().getController().getEditorPresentation().getController().setActiveModelPresentationForActiveCanvas(modelPresentation)); } private void handleRemovedModelPresentation(final HighLevelModelPresentation modelPresentation) { // If we remove the modelPresentation active on the canvas - if (EcdarController.getActiveCanvasPresentation().getController().getActiveModelPresentation() == modelPresentation) { + if (Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getActiveModelPresentation() == modelPresentation) { if (project.getComponents().size() > 0) { // Find the first available component and show it instead of the removed one final HighLevelModelPresentation newActiveModelPresentation = modelPresentationMap.keySet().iterator().next(); - Ecdar.getPresentation().getController().setActiveModelPresentationForActiveCanvas(newActiveModelPresentation); + Ecdar.getPresentation().getController().getEditorPresentation().getController().setActiveModelPresentationForActiveCanvas(newActiveModelPresentation); modelPresentationMap.get(newActiveModelPresentation).getController().setIsActive(true); } else { // Show no components (since there are none in the project) - Ecdar.getPresentation().getController().setActiveModelPresentationForActiveCanvas(null); + Ecdar.getPresentation().getController().getEditorPresentation().getController().setActiveModelPresentationForActiveCanvas(null); } } @@ -504,12 +504,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 ee0b0524..7602378e 100644 --- a/src/main/java/ecdar/controllers/QueryController.java +++ b/src/main/java/ecdar/controllers/QueryController.java @@ -3,17 +3,21 @@ import com.jfoenix.controls.JFXComboBox; import com.jfoenix.controls.JFXRippler; import ecdar.backend.Engine; +import com.jfoenix.controls.JFXTextField; 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,14 @@ 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((observable, oldValue, newValue) -> queryText.setText(StringHelper.ConvertSymbolsToUnicode(newValue))); } public void setQuery(Query 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..b3b79643 --- /dev/null +++ b/src/main/java/ecdar/controllers/RightSimPaneController.java @@ -0,0 +1,88 @@ +package ecdar.controllers; + +import ecdar.abstractions.Edge; +import ecdar.abstractions.Decision; +import ecdar.presentations.DecisionPresentation; +import ecdar.utility.colors.Color; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.fxml.Initializable; +import javafx.geometry.Insets; +import javafx.scene.layout.*; + +import java.net.URL; +import java.util.List; +import java.util.ResourceBundle; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Controller class for the right pane in the simulator + */ +public class RightSimPaneController implements Initializable { + public StackPane root; + public VBox availableDecisionsVBox; + + private Consumer onDecisionSelected; + private final ObservableList availableDecisions = FXCollections.observableArrayList(); + + @Override + public void initialize(URL location, ResourceBundle resources) { + initializeBackground(); + initializeLeftBorder(); + initializeDecisionHandling(); + } + + /** + * Sets the background color of the ScrollPane Vbox + */ + private void initializeBackground() { + availableDecisionsVBox.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() { + root.setBorder(new Border(new BorderStroke( + Color.GREY_BLUE.getColor(Color.Intensity.I900), + BorderStrokeStyle.SOLID, + CornerRadii.EMPTY, + new BorderWidths(0, 0, 0, 1) + ))); + } + + private void initializeDecisionHandling() { + availableDecisions.addListener((ListChangeListener) c -> { + while (c.next()) { + c.getRemoved().forEach(decisionPresentation -> Platform.runLater(() -> availableDecisionsVBox.getChildren().remove(decisionPresentation))); + + c.getAddedSubList().forEach(decisionPresentation -> { + // Request next step when decision is clicked + decisionPresentation.setOnMouseClicked(event -> onDecisionSelected.accept(decisionPresentation.getController().getDecision())); + Platform.runLater(() -> availableDecisionsVBox.getChildren().add(decisionPresentation)); + }); + } + }); + } + + public void setOnDecisionSelected(Consumer decisionSelected) { + onDecisionSelected = decisionSelected; + } + + public void setDecisionsList(List decisions) { + availableDecisions.setAll(decisions); + } + + protected List getDecisions() { + return availableDecisions.stream() + .map(decisionPresentation -> decisionPresentation.getController().getDecision()) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/ecdar/controllers/SignatureArrowController.java b/src/main/java/ecdar/controllers/SignatureArrowController.java index 791ccd02..3b0bdc8f 100644 --- a/src/main/java/ecdar/controllers/SignatureArrowController.java +++ b/src/main/java/ecdar/controllers/SignatureArrowController.java @@ -22,7 +22,6 @@ public class SignatureArrowController { private final ObjectProperty component = new SimpleObjectProperty<>(); private final ObjectProperty edgeStatus = new SimpleObjectProperty<>(); private final StringProperty syncText = new SimpleStringProperty(); - public Label signatureArrowLabel; public Path signatureArrowPath; public Path signatureArrowHeadPath; @@ -32,7 +31,6 @@ public class SignatureArrowController { public void initialize(final URL location, final ResourceBundle resources) { } - /*** * Finds matching edges and highlights them */ diff --git a/src/main/java/ecdar/controllers/SimEdgeController.java b/src/main/java/ecdar/controllers/SimEdgeController.java new file mode 100755 index 00000000..64ae1cc7 --- /dev/null +++ b/src/main/java/ecdar/controllers/SimEdgeController.java @@ -0,0 +1,432 @@ +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.presentations.SimulationPresentation; +import ecdar.utility.Highlightable; +import ecdar.utility.colors.EnabledColor; +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 SimulationPresentation} + */ +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().isHighlighted()) { + this.highlight(); + } else { + this.unhighlight(); + } + }); + + // When an edge updates highlight property, + // we want to update the view to reflect current highlight property + edge.get().isHighlightedForReachabilityProperty().addListener(v -> { + if (edge.get().getIsHighlightedForReachability()) { + this.highlightSpecialColor(); + } else { + this.unhighlight(); + } + }); + }); + + ensureNailsInFront(); + } + + public void highlightSpecialColor() { + edgeRoot.getChildren().forEach(node -> { + if (node instanceof Highlightable) { + ((Highlightable) node).highlightPurple(); + } + }); + } + + 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 + */ + public void color(final EnabledColor color) { + final Edge edge = getEdge(); + + // Set the color of the edge + edge.setColor(color); + } + + public EnabledColor getColor() { + return getEdge().getColor(); + } + + 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..2f4bffd1 --- /dev/null +++ b/src/main/java/ecdar/controllers/SimLocationController.java @@ -0,0 +1,305 @@ +package ecdar.controllers; + +import com.jfoenix.controls.JFXPopup; +import ecdar.Ecdar; +import ecdar.abstractions.*; +import ecdar.presentations.DropDownMenu; +import ecdar.presentations.SimLocationPresentation; +import ecdar.presentations.SimTagPresentation; +import ecdar.abstractions.State; +import ecdar.presentations.SimulationPresentation; +import ecdar.utility.colors.EnabledColor; +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 SimulationPresentation} + */ +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; + + public static String getSimLocationReachableQuery(final Location endLocation, final Component component, final String query) { + return getSimLocationReachableQuery(endLocation, component, query, null); + } + + /** + * Generates a reachability query based on the given location and component + * + * @param endLocation The location which should be checked for reachability + * @return A reachability query string + */ + public static String getSimLocationReachableQuery(final Location endLocation, final Component component, final String query, final State state) { + var stringBuilder = new StringBuilder(); + + // append simulation query + stringBuilder.append(query); + + // append arrow + stringBuilder.append(" -> "); + + // ToDo: append start location here + if (state != null) { + stringBuilder.append(getInitialStateString(state)); + stringBuilder.append(";"); + } + + // append end state + stringBuilder.append(getEndStateString(component.getName(), endLocation.getId())); + + // return example: m1||M2->[L1,L4](y<3);[L2, L7](y<2) + System.out.println(stringBuilder); + return stringBuilder.toString(); + } + + /** + * Returns a string representation of an array of initial location IDs for all components in the simulation + * + * @param state + * @return + */ + private static String getInitialStateString(State state) { + var initialStateStringBuilder = new StringBuilder(); + + // append locations + initialStateStringBuilder.append("["); + var appendLocationWithSeparator = false; + + // ToDO NIELS: Determine how to process this, if it is indeed the initial locations + for (var component : SimulationController.getSimulatedComponents()) { + boolean appendSeparator = appendLocationWithSeparator; + state.getComponentLocationMap().entrySet().stream() + // Should only match one component + .filter((componentLocationPair) -> componentLocationPair.getKey().equals(component.getName())) + .forEach((entry) -> { + String compName = entry.getKey(), locId = entry.getValue(); + + if (compName.equals(component.getName())) { + if (appendSeparator) { + initialStateStringBuilder.append(",") + .append(locId); + } else { + initialStateStringBuilder.append(locId); + } + } + }); + + appendLocationWithSeparator = true; + } + initialStateStringBuilder.append("]"); + + // ToDo: Add clocks + initialStateStringBuilder.append("()"); + + return initialStateStringBuilder.toString(); + } + + private static String getEndStateString(String componentName, String endLocationId) { + var stringBuilder = new StringBuilder(); + + stringBuilder.append("["); + var appendLocationWithSeparator = false; + + for (var component : SimulationController.getSimulatedComponents()) { + if (component.getName().equals(componentName)) { + if (appendLocationWithSeparator) { + stringBuilder.append(",") + .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("]"); + stringBuilder.append("()"); + + return stringBuilder.toString(); + } + + @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); + + String composition; + State currentState; + + try { + composition = SimulationController.getComposition(); + currentState = SimulationController.getCurrentState(); + } catch (NullPointerException e) { + Ecdar.showToast("Unable to inittialize dropdown due to null simulation"); + return; + } + + dropDownMenu.addClickableListElement("Is " + getLocation().getId() + " reachable from initial state?", event -> { + // Generate the query from the backend + final String reachabilityQuery = getSimLocationReachableQuery(getLocation(), getComponent(), composition); + + // Add proper comment + final String reachabilityComment = "Is " + getLocation().getMostDescriptiveIdentifier() + " reachable from initial state?"; + + // Add new query for this location + final Query query = new Query(reachabilityQuery, reachabilityComment, QueryState.UNKNOWN); + query.setType(QueryType.REACHABILITY); + + // execute query + query.execute(); + + dropDownMenu.hide(); + }); + + dropDownMenu.addClickableListElement("Is " + getLocation().getId() + " reachable from current locations?", event -> { + // Generate the query from the backend ToDo NIELS: Remove static methods + final String reachabilityQuery = getSimLocationReachableQuery(getLocation(), getComponent(), composition, currentState); + + // Add proper comment + final String reachabilityComment = "Is " + getLocation().getMostDescriptiveIdentifier() + " reachable from current locations?"; + + // Add new query for this location + final Query query = new Query(reachabilityQuery, reachabilityComment, QueryState.UNKNOWN); + query.setType(QueryType.REACHABILITY); + + // execute query + query.execute(); + + 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 + */ + public void color(final EnabledColor color) { + final Location location = getLocation(); + + // Set the color of the location + location.setColor(color); + } + + public EnabledColor getColor() { + return getLocation().getColor(); + } + + 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..bff7bad1 --- /dev/null +++ b/src/main/java/ecdar/controllers/SimNailController.java @@ -0,0 +1,122 @@ +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.EnabledColor; +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 EnabledColor getColor() { + return getComponent().getColor(); + } + + 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/SimulationController.java b/src/main/java/ecdar/controllers/SimulationController.java new file mode 100644 index 00000000..f9ef26c9 --- /dev/null +++ b/src/main/java/ecdar/controllers/SimulationController.java @@ -0,0 +1,386 @@ +package ecdar.controllers; + +import EcdarProtoBuf.QueryProtos; +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXDialog; +import com.jfoenix.controls.JFXDialogLayout; +import ecdar.Ecdar; +import ecdar.backend.BackendHelper; +import ecdar.backend.StateFactory; +import ecdar.presentations.*; +import ecdar.utility.helpers.SimulationHighlighter; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; + +import ecdar.abstractions.*; +import ecdar.abstractions.State; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableMap; +import javafx.event.EventHandler; +import javafx.fxml.Initializable; +import javafx.geometry.Insets; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.StackPane; + +import java.net.URL; +import java.util.*; +import java.util.stream.Collectors; + +public class SimulationController implements ModeController, Initializable { + public StackPane root; + public StackPane toolbar; + public ScrollPane scrollPane; + public Group groupContainer; + public FlowPane processContainer; + + public final LeftSimPanePresentation leftSimPane = new LeftSimPanePresentation(); + public final RightSimPanePresentation rightSimPane = new RightSimPanePresentation(this::nextStep); + + private static final ObjectProperty activeSimulation = new SimpleObjectProperty<>(null); + private static final ObjectProperty selectedState = new SimpleObjectProperty<>(); + + private SimulationHighlighter highlighter; + + /** + * 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 side. + */ + private static final int SCROLLPANE_OFFSET = 20; + + /** + * 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; + + private final EventHandler decisionOnMouseEnter = event -> highlighter.highlightDecisionEdges(((DecisionPresentation) event.getTarget())); + private final EventHandler decisionOnMouseExit = event -> highlighter.unhighlightEdges(); + + private final ObservableMap componentNameProcessPresentationMap = FXCollections.observableHashMap(); + private final StateFactory stateFactory = new StateFactory(); + + private JFXDialog restartSimConfirmDialog; + + @Override + public void initialize(final URL location, final ResourceBundle resources) { + //In case that the processContainer gets moved around we have to keep it in place. + initializeProcessContainer(); + initializeWindowResizing(); + initializeZoom(); + initializeRestartSimulationConfirmationDialog(); + + highlighter = new SimulationHighlighter(componentNameProcessPresentationMap, activeSimulation); + } + + private void initializeRestartSimulationConfirmationDialog() { + JFXDialogLayout layout = new JFXDialogLayout(); + layout.setHeading(new Label("Restart Simulation")); + + Label body = new Label("Restarting the simulation will remove all steps in the current trace.\n\nAre you sure you want to restart the simulation?"); + body.getStyleClass().add("body1"); + layout.setBody(body); + + restartSimConfirmDialog = new JFXDialog(new StackPane(), layout, JFXDialog.DialogTransition.CENTER); + JFXButton confirm = new JFXButton("Yes"); + confirm.setOnAction((event) -> { + restartSimConfirmDialog.close(); + resetSimulation(getActiveSimulation().composition); + }); + + JFXButton cancel = new JFXButton("No"); + cancel.setOnAction((event) -> { + restartSimConfirmDialog.close(); + }); + + layout.setActions(cancel, confirm); + } + + /** + * Initializes the {@link #processContainer} with its correct styling, and placement on the view. + * It also adds a {@link ListChangeListener} on the list of simulated components 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)); + } + + /** + * 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); + }); + } + + /** + * 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 - SCROLLPANE_OFFSET) * (1 + (1 - processContainer.getScaleX()))); + processContainer.setMaxWidth(processContainer.getWidth() + (deltaWidth - SCROLLPANE_OFFSET) * (1 + (1 - processContainer.getScaleX()))); + } else { // Reset + processContainer.setMinWidth(newValue.doubleValue() - SCROLLPANE_OFFSET); + processContainer.setMaxWidth(newValue.doubleValue() - SCROLLPANE_OFFSET); + } + }); + } + + /** + * Initializes the simulation based on the received proto response and current composition + * + * @param response ProtoBuf response received from a step request + * @param composition current composition being simulated + */ + private void initializeActiveSimulation(QueryProtos.SimulationStepResponse response, String composition) { + State initialState = this.stateFactory.createInitialState(composition, response); + + Simulation newSimulation = new Simulation(composition, initialState); + activeSimulation.set(newSimulation); + + newSimulation.simulatedComponents.forEach(component -> { + var processPresentation = new ProcessPresentation(component); + componentNameProcessPresentationMap.put(component.getName(), processPresentation); + processContainer.getChildren().addAll(processPresentation); + }); + + leftSimPane.getController().setTraceLog(newSimulation.traceLog); + Platform.runLater(() -> leftSimPane.getController() + .tracePanePresentation.getController() + .restartSimulation.setOnMouseClicked((event) -> { + event.consume(); + restartSimConfirmDialog.show(Ecdar.getPresentation()); + })); + + rightSimPane.getController().setDecisionsList( + initialState.getDecisions().stream() + .map(DecisionPresentation::new) + .collect(Collectors.toList()) + ); + + updateSimulationState(); + + // Update highlighting and decisions when state changes + newSimulation.currentState.addListener((observable, oldState, newState) -> { + updateSimulationVariablesAndClocks(); + highlighter.updateHighlighting(); + highlighter.fadeSucceedingTraceStates(leftSimPane.getController().tracePanePresentation.getController().traceList.getChildren()); + + rightSimPane.getController().setDecisionsList( + newState.getDecisions().stream() + .map(DecisionPresentation::new) + .collect(Collectors.toList()) + ); + }); + + rightSimPane.getController().availableDecisionsVBox.getChildren().addListener((ListChangeListener) c -> { + while (c.next()) { + c.getAddedSubList().forEach(decisionPresentation -> { + decisionPresentation.setOnMouseEntered(decisionOnMouseEnter); + decisionPresentation.setOnMouseExited(decisionOnMouseExit); + }); + } + }); + } + + public static State getCurrentState() throws NullPointerException { + return activeSimulation.get().currentState.get(); + } + + public static List getSimulatedComponents() { + return activeSimulation.get().simulatedComponents; + } + + private Simulation getActiveSimulation() { + return activeSimulation.get(); + } + + private List getProcessPresentations() { + return new ArrayList<>(componentNameProcessPresentationMap.values()); + } + + /** + * Reloads the whole simulation sets the initial transitions, states, etc + * + * @param composition current composition being simulated + */ + public void initialStep(String composition) { + BackendHelper.getDefaultEngine().enqueueRequest(new Decision(composition), + (response) -> Platform.runLater(() -> initializeActiveSimulation(response, composition)), + (error) -> Ecdar.showToast("Could not start simulation:\n" + error.getMessage())); + } + + /** + * Take a step in the simulation. + * + * @param decision basis of the step to take + */ + public void nextStep(Decision decision) { + // removes invalid states from the log when stepping forward after previewing a previous state + removeStatesFromLog(getActiveSimulation().traceLog.filtered(statePresentation -> statePresentation.getController().getState() == getActiveSimulation().currentState.get()).stream().findFirst().orElse(null)); + + BackendHelper.getDefaultEngine().enqueueRequest(decision, + (response) -> { + getActiveSimulation().currentState.set(stateFactory.createState(getComposition(), response)); + updateSimulationState(); + }, + (error) -> Ecdar.showToast("Could not take next step in simulation\nError: " + error.getMessage())); + } + + /** + * Resets the current simulation, and prepares for a new simulation by clearing the + * {@link SimulationController#processContainer} and adding the processes of the new simulation. + */ + public void resetSimulation(String queryToSimulate) { + clearOverview(); + initialStep(queryToSimulate); + } + + private void previewState(final State state) throws NullPointerException { + getActiveSimulation().currentState.set(state); + highlighter.fadeSucceedingTraceStates(leftSimPane.getController().tracePanePresentation.getController().traceList.getChildren()); + } + + private void updateSimulationState() { + Platform.runLater(() -> { + getActiveSimulation().addStateToTraceLog(getActiveSimulation().currentState.get(), (statePresentation) -> this.previewState(statePresentation.getController().getState())); + + updateSimulationVariablesAndClocks(); + highlighter.updateHighlighting(); + highlighter.fadeSucceedingTraceStates(leftSimPane.getController().tracePanePresentation.getController().traceList.getChildren()); + }); + } + + /** + * Setup listeners for displaying clock and variable values on the {@link ProcessPresentation} + */ + private void updateSimulationVariablesAndClocks() { + getActiveSimulation().variables.forEach((var, val) -> { + if (var.equals("t(0)")) {// As t(0) does not belong to any process + final String[] spittedString = var.split("\\."); + // If the process containing the var is not there we just skip it + if (spittedString.length > 0 && componentNameProcessPresentationMap.size() > 0) { + componentNameProcessPresentationMap.entrySet().stream() + .filter(processPair -> processPair.getKey().equals(spittedString[0])) + .findFirst().get().getValue().getController().getVariables() + .put(spittedString[1], val); + } + } + }); + + if (componentNameProcessPresentationMap.size() == 0) return; + getActiveSimulation().clocks.forEach((clock, val) -> { + if (!clock.equals("t(0)")) {// As t(0) does not belong to any process + final String[] spittedString = clock.split("\\."); + // If the process containing the clock is not there we just skip it + if (spittedString.length > 0 && componentNameProcessPresentationMap.size() > 0) { + componentNameProcessPresentationMap.entrySet().stream() + .filter(processPair -> processPair.getKey().equals(spittedString[0])) + .findFirst().get().getValue().getController().getClocks() + .put(spittedString[1], val); + } + } + }); + } + + /** + * Clears the {@link #processContainer} and the {@link #componentNameProcessPresentationMap}. + */ + private void clearOverview() { + processContainer.getChildren().clear(); + componentNameProcessPresentationMap.clear(); + } + + /** + * 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() - SCROLLPANE_OFFSET); + processContainer.setMaxWidth(scrollPane.getWidth() - 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))); + } + } + + /** + * Removes all states from the trace log, succeeding the given state + */ + private void removeStatesFromLog(StatePresentation statePresentation) { + if (statePresentation == null) return; + + var newLastStateIndex = getActiveSimulation().traceLog.indexOf(statePresentation); + getActiveSimulation().traceLog.remove(newLastStateIndex + 1, getActiveSimulation().traceLog.size()); + } + + public static void setSelectedState(State selectedState) { + SimulationController.selectedState.set(selectedState); + } + + public static String getComposition() { + return activeSimulation.get().composition; + } + + @Override + public StackPane getLeftPane() { + return leftSimPane; + } + + @Override + public StackPane getRightPane() { + return rightSimPane; + } +} diff --git a/src/main/java/ecdar/controllers/SimulationInitializationDialogController.java b/src/main/java/ecdar/controllers/SimulationInitializationDialogController.java new file mode 100644 index 00000000..a3d0d905 --- /dev/null +++ b/src/main/java/ecdar/controllers/SimulationInitializationDialogController.java @@ -0,0 +1,17 @@ +package ecdar.controllers; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXComboBox; +import javafx.fxml.Initializable; + +import java.net.URL; +import java.util.*; + +public class SimulationInitializationDialogController implements Initializable { + public JFXComboBox simulationComboBox; + public JFXButton cancelButton; + public JFXButton startButton; + + public void initialize(URL location, ResourceBundle resources) { + } +} diff --git a/src/main/java/ecdar/controllers/StateController.java b/src/main/java/ecdar/controllers/StateController.java new file mode 100755 index 00000000..0310d44c --- /dev/null +++ b/src/main/java/ecdar/controllers/StateController.java @@ -0,0 +1,79 @@ +package ecdar.controllers; + +import com.jfoenix.controls.JFXRippler; +import ecdar.abstractions.State; +import ecdar.utility.helpers.ClockConstraintsHandler; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.Initializable; +import javafx.scene.control.Label; +import javafx.scene.layout.AnchorPane; +import javafx.scene.text.Text; + +import java.net.URL; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * The controller class for the state view element. + * It represents a single state within the trace of the simulation + */ +public class StateController implements Initializable { + public AnchorPane root; + public Label locations; + public Text clockConstraintsHeader; + public Label clockConstraints; + public JFXRippler rippler; + + // The transition that the view represents + private final SimpleObjectProperty state = new SimpleObjectProperty<>(); + + private final ClockConstraintsHandler clockConstraintsHandler = new ClockConstraintsHandler(); + + @Override + public void initialize(URL location, ResourceBundle resources) { + state.addListener((observable, oldValue, newValue) -> { + locations.setText(getStateLocationsString(newValue)); + + setClockConstraintsSectionVisibility(!newValue.getClockConstraints().isEmpty()); + clockConstraints.setText(clockConstraintsHandler.getStateClockConstraintsString(newValue.getClockConstraints())); + }); + } + + public void setState(State state) { + this.state.set(state); + } + + public State getState() { + return state.get(); + } + + private void setClockConstraintsSectionVisibility(boolean visibility) { + clockConstraintsHeader.setVisible(visibility); + clockConstraintsHeader.setManaged(visibility); + clockConstraints.setVisible(visibility); + clockConstraints.setManaged(visibility); + } + + /** + * A helper method that returns a string representing the locations of a state in the trace log + * + * @return A string representing the locations + */ + private String getStateLocationsString(State state) { + StringBuilder locationsString = new StringBuilder(); + + int i = 0; + for (Map.Entry componentLocation : state.getComponentLocationMap().entrySet()) { + locationsString.append(componentLocation.getKey()); + locationsString.append('.'); + locationsString.append(componentLocation.getValue()); + + i++; + if (i < state.getComponentLocationMap().size()) { + locationsString.append(System.lineSeparator()); + } + } + + return locationsString.toString(); + } +} diff --git a/src/main/java/ecdar/controllers/SystemController.java b/src/main/java/ecdar/controllers/SystemController.java index 9fa85828..49a0f596 100644 --- a/src/main/java/ecdar/controllers/SystemController.java +++ b/src/main/java/ecdar/controllers/SystemController.java @@ -189,7 +189,7 @@ private void initializeBackground() { */ @FXML private void modelContainerPressed(final MouseEvent event) { - EcdarController.getActiveCanvasPresentation().getController().leaveTextAreas(); + Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().leaveTextAreas(); SelectHelper.clearSelectedElements(); if (event.isSecondaryButtonDown()) { diff --git a/src/main/java/ecdar/controllers/TracePaneController.java b/src/main/java/ecdar/controllers/TracePaneController.java new file mode 100755 index 00000000..1c2e33c6 --- /dev/null +++ b/src/main/java/ecdar/controllers/TracePaneController.java @@ -0,0 +1,213 @@ +package ecdar.controllers; + +import com.jfoenix.controls.JFXRippler; +import com.jfoenix.controls.JFXTooltip; +import ecdar.abstractions.State; +import ecdar.presentations.StatePresentation; +import ecdar.utility.colors.Color; +import javafx.application.Platform; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ChangeListener; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.geometry.Insets; +import javafx.scene.Cursor; +import javafx.scene.control.Label; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.*; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.net.URL; +import java.util.ResourceBundle; +import java.util.function.BiConsumer; + +/** + * The controller class for the trace pane element that can be inserted into a simulator pane + */ +public class TracePaneController implements Initializable { + public VBox root; + public HBox toolbar; + public Label traceTitle; + public JFXRippler expandTrace; + public JFXRippler restartSimulation; + public VBox traceSection; + public VBox traceList; + public FontIcon expandTraceIcon; + public AnchorPane traceSummary; + public Label summaryTitleLabel; + public Label summarySubtitleLabel; + + private final SimpleBooleanProperty isTraceExpanded = new SimpleBooleanProperty(false); + private ObservableList traceLog; + + /** + * Keep reference to the traceLogListener, such that it can be removed and added to future logs + */ + private final ListChangeListener traceLogListener = c -> { + updateSummaryTitle(traceLog.size()); + + if (!isTraceExpanded.get()) return; + while (c.next()) { + c.getRemoved().forEach(statePresentation -> traceList.getChildren().remove(statePresentation)); + c.getAddedSubList().forEach(this::insertStateInTrace); + } + }; + + + @Override + public void initialize(URL location, ResourceBundle resources) { + initializeToolbar(); + initializeSummaryView(); + + isTraceExpanded.addListener((obs, oldVal, newVal) -> { + if (newVal) { + Platform.runLater(this::showTrace); + expandTraceIcon.setIconLiteral("gmi-expand-less"); + } else { + Platform.runLater(this::hideTrace); + expandTraceIcon.setIconLiteral("gmi-expand-more"); + } + + traceSection.setManaged(newVal); + }); + + Platform.runLater(this::toggleTraceExpand); + } + + /** + * Initializes the toolbar 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; + + toolbar.setBackground(new Background(new BackgroundFill( + color.getColor(colorIntensity), + CornerRadii.EMPTY, + Insets.EMPTY))); + traceTitle.setTextFill(color.getTextColor(colorIntensity)); + + expandTrace.setMaskType(JFXRippler.RipplerMask.CIRCLE); + expandTrace.setRipplerFill(color.getTextColor(colorIntensity)); + + JFXTooltip.install(restartSimulation, new JFXTooltip("Restart Simulation")); + } + + /** + * Initializes the summary view to be updated when steps are taken in the trace. + * Also changes the color and cursor when mouse enters and exits the summary view. + */ + private void initializeSummaryView() { + final Color color = Color.GREY_BLUE; + final Color.Intensity colorIntensity = Color.Intensity.I50; + + final BiConsumer setBackground = (newColor, newIntensity) -> { + traceSummary.setBackground(new Background(new BackgroundFill( + newColor.getColor(newIntensity), + CornerRadii.EMPTY, + Insets.EMPTY + ))); + + 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 + traceSummary.setOnMouseEntered(event -> { + setBackground.accept(color, colorIntensity.next()); + root.setCursor(Cursor.HAND); + }); + + // Update the background when the mouse exits + traceSummary.setOnMouseExited(event -> { + setBackground.accept(color, colorIntensity); + root.setCursor(Cursor.DEFAULT); + }); + + // Update the background initially + setBackground.accept(color, colorIntensity); + } + + protected void setTraceLog(ObservableList traceLog) { + if (this.traceLog != null) { + // Remove all information from previous simulation run + this.traceLog.removeListener(traceLogListener); + this.traceLog.clear(); + }; + + this.traceList.getChildren().clear(); + this.traceLog = traceLog; + + // Add listener to new log + this.traceLog.addListener(traceLogListener); + } + + /** + * 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) { + summaryTitleLabel.setText(steps + " number of steps in trace"); + } + + /** + * 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 StatePresentation} for each trace state + * Also hides the summary view, since it should only be visible when the trace is hidden + */ + private void showTrace() { + root.getChildren().remove(traceSummary); + // After initialization, the log will be null + if (this.traceLog != null) this.traceLog.forEach(this::insertStateInTrace); + } + + /** + * Instantiates a {@link StatePresentation} for a {@link State} and adds it to the view + * + * @param statePresentation The statePresentation to insert into the trace log + */ + private void insertStateInTrace(final StatePresentation statePresentation) { + // Install mouse event listeners on the state + EventHandler mouseEntered = statePresentation.getOnMouseEntered(); + statePresentation.setOnMouseEntered(event -> { + SimulationController.setSelectedState(statePresentation.getController().getState()); + mouseEntered.handle(event); + }); + + EventHandler mouseExited = statePresentation.getOnMouseExited(); + statePresentation.setOnMouseExited(event -> { + SimulationController.setSelectedState(null); + mouseExited.handle(event); + }); + + // Only insert the presentation into the view if the trace is expanded + if (isTraceExpanded.get()) { + traceList.getChildren().add(0, statePresentation); + } + } + + /** + * Method to be called when clicking on the expand rippler in the trace toolbar + */ + @FXML + private void toggleTraceExpand() { + isTraceExpanded.set(!isTraceExpanded.get()); + } +} diff --git a/src/main/java/ecdar/model_canvas/arrow_heads/SimpleArrowHead.java b/src/main/java/ecdar/model_canvas/arrow_heads/SimpleArrowHead.java index ced90b68..8217b8db 100644 --- a/src/main/java/ecdar/model_canvas/arrow_heads/SimpleArrowHead.java +++ b/src/main/java/ecdar/model_canvas/arrow_heads/SimpleArrowHead.java @@ -91,6 +91,12 @@ public void highlight() { rightArrow.setStroke(SelectHelper.getNormalColor()); } + @Override + public void highlightPurple() { + leftArrow.setStroke(Color.DEEP_PURPLE.getColor(Color.Intensity.I900)); + rightArrow.setStroke(Color.DEEP_PURPLE.getColor(Color.Intensity.I900)); + } + /*** * Removes the highlight by setting the stroke to grey */ diff --git a/src/main/java/ecdar/mutation/ExportHandler.java b/src/main/java/ecdar/mutation/ExportHandler.java index f4ef5420..9f943a89 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/MutationTestPlanPresentation.java b/src/main/java/ecdar/mutation/MutationTestPlanPresentation.java index 3d03d032..8f299be8 100644 --- a/src/main/java/ecdar/mutation/MutationTestPlanPresentation.java +++ b/src/main/java/ecdar/mutation/MutationTestPlanPresentation.java @@ -551,19 +551,19 @@ private void initializeModelPicker() { * Initializes width and height of the text editor field, such that it fills up the whole canvas. */ private void initializeWidthAndHeight() { - controller.scrollPane.setPrefWidth(EcdarController.getActiveCanvasPresentation().getController().getWidthProperty().doubleValue()); - EcdarController.getActiveCanvasPresentation().getController().getWidthProperty().addListener((observable, oldValue, newValue) -> + controller.scrollPane.setPrefWidth(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getWidthProperty().doubleValue()); + Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getWidthProperty().addListener((observable, oldValue, newValue) -> controller.scrollPane.setPrefWidth(newValue.doubleValue())); - updateOffset(EcdarController.getActiveCanvasPresentation().getController().getInsetShouldShow().get()); - EcdarController.getActiveCanvasPresentation().getController().getInsetShouldShow().addListener((observable, oldValue, newValue) -> { + updateOffset(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getInsetShouldShow().get()); + Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getInsetShouldShow().addListener((observable, oldValue, newValue) -> { updateOffset(newValue); updateHeight(); }); - canvasHeight = EcdarController.getActiveCanvasPresentation().getController().getHeightProperty().doubleValue(); + canvasHeight = Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getHeightProperty().doubleValue(); updateHeight(); - EcdarController.getActiveCanvasPresentation().getController().getHeightProperty().addListener((observable, oldValue, newValue) -> { + Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getHeightProperty().addListener((observable, oldValue, newValue) -> { canvasHeight = newValue.doubleValue(); updateHeight(); }); @@ -590,7 +590,7 @@ private void updateOffset(final boolean shouldHave) { * Updates the height of the view. */ private void updateHeight() { - controller.scrollPane.setPrefHeight(canvasHeight - EcdarController.getActiveCanvasPresentation().getController().DECLARATION_Y_MARGIN - offSet); + controller.scrollPane.setPrefHeight(canvasHeight - Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().DECLARATION_Y_MARGIN - offSet); } /** diff --git a/src/main/java/ecdar/mutation/TestCaseGenerationHandler.java b/src/main/java/ecdar/mutation/TestCaseGenerationHandler.java index 7d610874..b0dcbdb9 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; @@ -224,7 +222,6 @@ private void generateTestCase(final MutationTestCase testCase, final int tries) * @throws BackendException if the backend encounters an error */ private Process startProcessToFetchStrategy(final String modelPath) throws BackendException { - // ToDo NIELS: not implemented after switch to thread pool // Run backend to check refinement and to fetch strategy if non-refinement // return ((jECDARDriver) BackendDriverManager.getInstance(BackendHelper.BackendNames.jEcdar)).getJEcdarProcessForRefinementCheckAndStrategyIfNonRefinement(modelPath, queryFilePath); return null; diff --git a/src/main/java/ecdar/mutation/TextFlowBuilder.java b/src/main/java/ecdar/mutation/TextFlowBuilder.java index 915355be..291f1641 100644 --- a/src/main/java/ecdar/mutation/TextFlowBuilder.java +++ b/src/main/java/ecdar/mutation/TextFlowBuilder.java @@ -4,7 +4,6 @@ import ecdar.abstractions.Component; import ecdar.abstractions.Edge; import ecdar.abstractions.Location; -import ecdar.controllers.EcdarController; import ecdar.utility.helpers.SelectHelper; import javafx.application.Platform; import javafx.collections.FXCollections; @@ -94,7 +93,7 @@ public static Text createLocationLink(final String locationId, final String comp } SelectHelper.elementsToBeSelected = FXCollections.observableArrayList(); - Ecdar.getPresentation().getController().setActiveModelPresentationForActiveCanvas(Ecdar.getComponentPresentationOfComponent(component)); + Ecdar.getPresentation().getController().getEditorPresentation().getController().setActiveModelPresentationForActiveCanvas(Ecdar.getComponentPresentationOfComponent(component)); // Use a list, since there could be multiple locations (e.i. Universal locations) final List locations = component.getLocations().filtered(loc -> loc.getId().equals(locationId)); diff --git a/src/main/java/ecdar/presentations/CanvasPresentation.java b/src/main/java/ecdar/presentations/CanvasPresentation.java index 959dacdc..726e85a3 100644 --- a/src/main/java/ecdar/presentations/CanvasPresentation.java +++ b/src/main/java/ecdar/presentations/CanvasPresentation.java @@ -2,6 +2,7 @@ import com.jfoenix.controls.JFXRippler; import com.jfoenix.skins.ValidationPane; +import ecdar.Ecdar; import ecdar.controllers.CanvasController; import ecdar.controllers.EcdarController; import ecdar.utility.colors.Color; @@ -36,7 +37,7 @@ public CanvasPresentation() { mouseTracker = new MouseTracker(this); mouseTracker.registerOnMousePressedEventHandler(this::startDragSelect); - getController().root.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> EcdarController.setActiveCanvasPresentation(this)); + getController().root.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> Ecdar.getPresentation().getController().getEditorPresentation().getController().setActiveCanvasPresentation(this)); initializeModelDrag(); initializeToolbar(); diff --git a/src/main/java/ecdar/presentations/ComponentInstancePresentation.java b/src/main/java/ecdar/presentations/ComponentInstancePresentation.java index ca58916b..e15df2d7 100644 --- a/src/main/java/ecdar/presentations/ComponentInstancePresentation.java +++ b/src/main/java/ecdar/presentations/ComponentInstancePresentation.java @@ -118,7 +118,7 @@ private void initializeName() { // Center the text vertically and aff a left padding of CORNER_SIZE controller.identifier.setPadding(new Insets(2, 0, 0, Ecdar.CANVAS_PADDING * 4)); - controller.identifier.setOnKeyPressed(EcdarController.getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler()); + controller.identifier.setOnKeyPressed(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler()); controller.originalComponentLabel.setPadding(new Insets(0, 5, 0, 15)); controller.originalComponentLabel.textProperty().bind(instance.getComponent().nameProperty()); diff --git a/src/main/java/ecdar/presentations/DecisionPresentation.java b/src/main/java/ecdar/presentations/DecisionPresentation.java new file mode 100644 index 00000000..376248a7 --- /dev/null +++ b/src/main/java/ecdar/presentations/DecisionPresentation.java @@ -0,0 +1,18 @@ +package ecdar.presentations; + +import ecdar.abstractions.Decision; +import ecdar.controllers.DecisionController; +import javafx.scene.layout.AnchorPane; + +public class DecisionPresentation extends AnchorPane { + private final DecisionController controller; + + public DecisionPresentation(Decision decision) { + controller = new EcdarFXMLLoader().loadAndGetController("DecisionPresentation.fxml", this); + controller.setDecision(decision); + } + + public DecisionController getController() { + return controller; + } +} diff --git a/src/main/java/ecdar/presentations/DropDownMenu.java b/src/main/java/ecdar/presentations/DropDownMenu.java index e76fb38f..3a389499 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; diff --git a/src/main/java/ecdar/presentations/EcdarFXMLLoader.java b/src/main/java/ecdar/presentations/EcdarFXMLLoader.java index 80280e8f..548b3901 100644 --- a/src/main/java/ecdar/presentations/EcdarFXMLLoader.java +++ b/src/main/java/ecdar/presentations/EcdarFXMLLoader.java @@ -11,7 +11,7 @@ */ public class EcdarFXMLLoader extends FXMLLoader { /** - * Loads an object hierarchy from from an XML document. + * Loads an object hierarchy from an XML document. * @param resourceName the name of the resource used to resolve relative path attribute values * @param type of controller to get * @param root the root of the object hierarchy diff --git a/src/main/java/ecdar/presentations/EcdarPresentation.java b/src/main/java/ecdar/presentations/EcdarPresentation.java index 0e45b3c8..c5d9ca4d 100644 --- a/src/main/java/ecdar/presentations/EcdarPresentation.java +++ b/src/main/java/ecdar/presentations/EcdarPresentation.java @@ -2,119 +2,97 @@ import com.jfoenix.controls.JFXSnackbarLayout; import ecdar.Ecdar; +import ecdar.abstractions.Component; import ecdar.abstractions.Query; import ecdar.abstractions.Snackbar; import ecdar.controllers.EcdarController; import ecdar.utility.UndoRedoStack; import ecdar.utility.colors.Color; -import ecdar.utility.colors.EnabledColor; import ecdar.utility.helpers.ImageScaler; -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; import javafx.animation.*; import javafx.application.Platform; 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.geometry.Insets; -import javafx.scene.control.Label; -import javafx.scene.control.Tooltip; import javafx.scene.image.Image; 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(true); + private final SimpleDoubleProperty leftPaneAnimationProperty = new SimpleDoubleProperty(0); + private final BooleanProperty rightPaneOpen = new SimpleBooleanProperty(true); + 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(); + EcdarController.currentMode.addListener(observable -> { + Platform.runLater(() -> { + initializeToggleLeftPaneFunctionality(); + initializeToggleRightPaneFunctionality(); + }); }); 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(); } }); }); + + // Trigger closing followed by opening of the left pane to ensure correct placement + closeLeftPaneAnimation.setOnFinished((e) -> openLeftPaneAnimation.play()); + closeLeftPaneAnimation.play(); }); initializeHelpImages(); KeyboardTracker.registerKeybind(KeyboardTracker.UNDO, new Keybind(new KeyCodeCombination(KeyCode.Z, KeyCombination.SHORTCUT_DOWN), UndoRedoStack::undo)); KeyboardTracker.registerKeybind(KeyboardTracker.REDO, new Keybind(new KeyCodeCombination(KeyCode.Z, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN), UndoRedoStack::redo)); - - initializeResizeQueryPane(); } private void initializeSnackbar() { @@ -123,147 +101,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.getPaintColor()); - circle.setStroke(color.getStrokeColor()); - 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, selectable.getColor())); - }); - - 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; @@ -276,75 +113,83 @@ private void initializeQueryDetailsDialog() { ))); } - private void initializeToggleFilePaneFunctionality() { - initializeOpenFilePaneAnimation(); - initializeCloseFilePaneAnimation(); + private void initializeToggleLeftPaneFunctionality() { + initializeOpenLeftPaneAnimation(); + initializeCloseLeftPaneAnimation(); // Translate the x coordinate to create the open/close animations - controller.projectPane.translateXProperty().bind(filePaneAnimationProperty.subtract(controller.projectPane.widthProperty())); + controller.getLeftModePane().translateXProperty().bind(leftPaneAnimationProperty.subtract(controller.getLeftModePane().widthProperty())); + + // Whenever the width of the file pane is updated, update the animations + controller.getLeftModePane().widthProperty().addListener((observable) -> { + initializeOpenLeftPaneAnimation(); + initializeCloseLeftPaneAnimation(); + }); // Whenever the width of the file pane is updated, update the animations - controller.projectPane.widthProperty().addListener((observable) -> { - initializeOpenFilePaneAnimation(); - initializeCloseFilePaneAnimation(); + // NOT USED, BUT ALLOWS FOR LEFT PANE RESIZING TO BE ADDED + 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.projectPane.getWidth(), interpolator); - final KeyValue closed = new KeyValue(filePaneAnimationProperty, 0, interpolator); + final KeyValue open = new KeyValue(leftPaneAnimationProperty, controller.getLeftModePane().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.projectPane.getWidth(), interpolator); + final KeyValue closed = new KeyValue(leftPaneAnimationProperty, 0, interpolator); + final KeyValue open = new KeyValue(leftPaneAnimationProperty, controller.getLeftModePane().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.getRightModePane().translateXProperty().bind(rightPaneAnimationProperty.multiply(-1).add(controller.getRightModePane().widthProperty())); // Whenever the width of the query pane is updated, update the animations - controller.queryPane.widthProperty().addListener((observable) -> { - initializeOpenQueryPaneAnimation(); - initializeCloseQueryPaneAnimation(); + controller.getRightModePane().widthProperty().addListener((observable, oldWidth, newWidth) -> { + rightPaneAnimationProperty.set(controller.getRightModePane().getWidth()); + initializeOpenRightPaneAnimation(); + initializeCloseRightPaneAnimation(); }); Platform.runLater(() -> { // 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(true); } }); } @@ -352,32 +197,32 @@ private void initializeToggleQueryPaneFunctionality() { }); } - 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.getRightModePane().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.getRightModePane().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() { @@ -400,18 +245,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. */ @@ -429,54 +262,30 @@ private void initializeHelpImages() { ImageScaler.fitImageToPane(controller.helpOutputImage, controller.helpOutputPane); } - private void initializeResizeQueryPane() { - final DoubleProperty prevX = new SimpleDoubleProperty(); - final DoubleProperty prevWidth = new SimpleDoubleProperty(); - - controller.queryPane.getController().resizeAnchor.setOnMousePressed(event -> { - event.consume(); - - prevX.set(event.getScreenX()); - prevWidth.set(controller.queryPane.getWidth()); - }); - - controller.queryPane.getController().resizeAnchor.setOnMouseDragged(event -> { - double diff = prevX.get() - event.getScreenX(); - - // 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); - 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 void showSnackbarMessage(final String message) { @@ -485,8 +294,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..e3abc899 --- /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, selectable.getColor())); + }); + + 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 e92a8813..de61861f 100644 --- a/src/main/java/ecdar/presentations/FilePresentation.java +++ b/src/main/java/ecdar/presentations/FilePresentation.java @@ -4,8 +4,6 @@ import ecdar.abstractions.HighLevelModel; import ecdar.controllers.FileController; import javafx.application.Platform; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; import javafx.scene.layout.*; public class FilePresentation extends AnchorPane { diff --git a/src/main/java/ecdar/presentations/LeftSimPanePresentation.java b/src/main/java/ecdar/presentations/LeftSimPanePresentation.java new file mode 100755 index 00000000..728d73af --- /dev/null +++ b/src/main/java/ecdar/presentations/LeftSimPanePresentation.java @@ -0,0 +1,16 @@ +package ecdar.presentations; + +import ecdar.controllers.LeftSimPaneController; +import javafx.scene.layout.*; + +public class LeftSimPanePresentation extends StackPane { + private final LeftSimPaneController controller; + + public LeftSimPanePresentation() { + controller = new EcdarFXMLLoader().loadAndGetController("LeftSimPanePresentation.fxml", this); + } + + public LeftSimPaneController getController() { + return controller; + } +} diff --git a/src/main/java/ecdar/presentations/Link.java b/src/main/java/ecdar/presentations/Link.java index d4e30147..8b5f6de4 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; @@ -157,4 +156,9 @@ public void deselect() { public void unhighlight() { shownLine.setStroke(Color.GREY.getColor(Color.Intensity.I900)); } + + @Override + public void highlightPurple() { + shownLine.setStroke(Color.DEEP_PURPLE.getColor(Color.Intensity.I900)); + } } diff --git a/src/main/java/ecdar/presentations/LocationPresentation.java b/src/main/java/ecdar/presentations/LocationPresentation.java index 042bc7be..ce6aee49 100644 --- a/src/main/java/ecdar/presentations/LocationPresentation.java +++ b/src/main/java/ecdar/presentations/LocationPresentation.java @@ -1,14 +1,15 @@ package ecdar.presentations; +import ecdar.Ecdar; import ecdar.abstractions.Component; import ecdar.abstractions.Location; -import ecdar.controllers.EcdarController; import ecdar.controllers.LocationController; import ecdar.utility.colors.Color; import ecdar.utility.colors.EnabledColor; import ecdar.utility.helpers.BindingHelper; import ecdar.utility.helpers.SelectHelper; import javafx.animation.*; +import javafx.application.Platform; import javafx.beans.binding.DoubleBinding; import javafx.beans.property.*; import javafx.scene.Group; @@ -21,7 +22,6 @@ import java.util.List; import java.util.function.BiConsumer; import java.util.function.Consumer; -import java.util.function.Function; import static javafx.util.Duration.millis; @@ -73,72 +73,6 @@ public LocationPresentation(final Location location, final Component component, initializeDeleteShakeAnimation(); initializeShakeAnimation(); initializeCircle(); - initializeReachabilityStyle(); - } - - private void initializeReachabilityStyle() { - - controller.reachabilityStatus.radiusProperty().bind(controller.circle.radiusProperty()); - controller.reachabilityStatus.setFill(javafx.scene.paint.Color.TRANSPARENT); - - final Function getColor = (reachability -> { - if (reachability == null || reachability.equals(Location.Reachability.REACHABLE)) - return javafx.scene.paint.Color.TRANSPARENT; - if (reachability.equals(Location.Reachability.EXCLUDED)) - return javafx.scene.paint.Color.TRANSPARENT; - if (reachability.equals(Location.Reachability.UNREACHABLE)) - return Color.RED.getColor(Color.Intensity.I700, 0.75); - if (reachability.equals(Location.Reachability.UNKNOWN)) - return Color.YELLOW.getColor(Color.Intensity.I900, 0.75); - - return null; - }); - - final Function getRadius = (reachability -> { - if (reachability == null || reachability.equals(Location.Reachability.REACHABLE)) return 0; - if (reachability.equals(Location.Reachability.EXCLUDED)) return 0; - if (reachability.equals(Location.Reachability.UNREACHABLE)) return 6; - if (reachability.equals(Location.Reachability.UNKNOWN)) return 3; - return 0; - }); - - final Consumer updateReachability = (reachability) -> { - if(reachability == null) return; - - final Interpolator interpolator = Interpolator.SPLINE(0.645, 0.045, 0.355, 1); - - // Shrink previous indicator - final Timeline shrinkAnimation = new Timeline(); - final KeyValue kv1 = new KeyValue(controller.reachabilityStatus.strokeWidthProperty(), controller.reachabilityStatus.getStrokeWidth(), interpolator); - final KeyValue kv2 = new KeyValue(controller.reachabilityStatus.strokeWidthProperty(), 0, interpolator); - final KeyFrame kf1 = new KeyFrame(millis(0), kv1); - final KeyFrame kf2 = new KeyFrame(millis(200), kv2); - shrinkAnimation.getKeyFrames().addAll(kf1, kf2); - - // Shrink current indicator - final Timeline enlargeAnimation = new Timeline(); - final KeyValue kv3 = new KeyValue(controller.reachabilityStatus.strokeWidthProperty(), 0, interpolator); - final KeyValue kv4 = new KeyValue(controller.reachabilityStatus.strokeWidthProperty(), getRadius.apply(reachability), interpolator); - final KeyFrame kf3 = new KeyFrame(millis(0), kv3); - final KeyFrame kf4 = new KeyFrame(millis(200), kv4); - enlargeAnimation.getKeyFrames().addAll(kf3, kf4); - - shrinkAnimation.setOnFinished(event -> { - // Update the color once the shrink animation is done - controller.reachabilityStatus.setStroke(getColor.apply(reachability)); - - // Play the enlarge animation - enlargeAnimation.play(); - }); - - shrinkAnimation.play(); - }; - - controller.getLocation().reachabilityProperty().addListener((obs, oldReachability, newReachability) -> { - updateReachability.accept(newReachability); - }); - - updateReachability.accept(controller.getLocation().getReachability()); } private void initializeIdLabel() { @@ -157,49 +91,61 @@ private void initializeIdLabel() { 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(); - // Delegate to style the label based on the color of the location final Consumer updateColor = (newColor) -> { - idLabel.setTextFill(newColor.getTextColor()); - ds.setColor(newColor.getPaintColor()); + if (location.getFailing()) { + idLabel.setTextFill(Color.RED.getTextColor(Color.Intensity.I700)); + ds.setColor(Color.RED.getColor(Color.Intensity.I700)); + } else { + idLabel.setTextFill(newColor.getTextColor()); + ds.setColor(newColor.getPaintColor()); + } }; updateColorDelegates.add(updateColor); // Set the initial color - updateColor.accept(color.get()); + updateColor.accept(location.getColor()); // Update the color of the circle when the color of the location is updated - color.addListener((obs, old, newColor) -> updateColor.accept(newColor)); + location.colorProperty().addListener((obs, old, newColor) -> updateColor.accept(newColor)); + location.failingProperty().addListener((obs, old, newFailing) -> updateColor.accept(location.getColor())); // RED if failing, provided color otherwise } private void initializeTags() { - if(!interactable) { + if (!interactable) { controller.nicknameTag.setVisible(false); controller.invariantTag.setVisible(false); return; } 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(); - if((loc.getNicknameX() != 0) && (loc.getNicknameY() != 0)) { + if ((loc.getNicknameX() != 0) && (loc.getNicknameY() != 0)) { controller.nicknameTag.setTranslateX(loc.getNicknameX()); controller.nicknameTag.setTranslateY(loc.getNicknameY()); } - if((loc.getInvariantX() != 0) && (loc.getInvariantY() != 0)) { + 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().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 @@ -218,7 +164,8 @@ private void initializeTags() { final Consumer updateVisibilityFromNickName = (nickname) -> { if (nickname.equals("") && !controller.nicknameTag.textFieldFocusProperty().get()) { controller.nicknameTag.setOpacity(0); - } else {controller.nicknameTag.setOpacity(1); + } else { + controller.nicknameTag.setOpacity(1); } }; @@ -258,8 +205,10 @@ private void initializeTags() { BindingHelper.bind(controller.invariantTagLine, controller.invariantTag); }; - controller.nicknameTag.setOnKeyPressed(EcdarController.getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler()); - controller.invariantTag.setOnKeyPressed(EcdarController.getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler()); + Platform.runLater(() -> { + controller.nicknameTag.setOnKeyPressed(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler()); + controller.invariantTag.setOnKeyPressed(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler()); + }); // Update the tags when the loc updates controller.locationProperty().addListener(observable -> updateTags.accept(loc)); @@ -363,19 +312,19 @@ protected void interpolate(final double frac) { @Override protected void interpolate(final double frac) { - animation.set(1-frac); + animation.set(1 - frac); } }; boolean isNormalOrProhibited = newUrgency.equals(Location.Urgency.NORMAL) || newUrgency.equals(Location.Urgency.PROHIBITED); - if(!oldUrgency.equals(Location.Urgency.URGENT) && !isNormalOrProhibited) { + if (!oldUrgency.equals(Location.Urgency.URGENT) && !isNormalOrProhibited) { toUrgent.play(); - } else if(isNormalOrProhibited && oldUrgency.equals(Location.Urgency.URGENT)) { + } else if (isNormalOrProhibited && oldUrgency.equals(Location.Urgency.URGENT)) { toNormal.play(); } - if(newUrgency.equals(Location.Urgency.COMMITTED)) { + if (newUrgency.equals(Location.Urgency.COMMITTED)) { committedShape.setVisible(true); notCommittedShape.setVisible(false); } else { @@ -383,7 +332,7 @@ protected void interpolate(final double frac) { notCommittedShape.setVisible(true); } - if(newUrgency.equals(Location.Urgency.PROHIBITED)) { + if (newUrgency.equals(Location.Urgency.PROHIBITED)) { notCommittedShape.setStrokeWidth(4); notCommittedShape.setStroke(Color.RED.getColor(Color.Intensity.A700)); controller.prohibitedLocStrikeThrough.setVisible(true); @@ -400,28 +349,33 @@ protected void interpolate(final double frac) { updateUrgencies.accept(Location.Urgency.NORMAL, location.getUrgency()); - // Update the colors - final ObjectProperty color = location.colorProperty(); - // Delegate to style the label based on the color of the location final Consumer updateColor = (newColor) -> { - notCommittedShape.setFill(newColor.getPaintColor()); - if (!location.getUrgency().equals(Location.Urgency.PROHIBITED)) { - notCommittedShape.setStroke(newColor.getStrokeColor()); + if (location.getFailing()) { + notCommittedShape.setFill(Color.RED.getColor(Color.Intensity.I700)); + } else { + notCommittedShape.setFill(newColor.getStrokeColor()); + } + } else if (location.getFailing()) { + notCommittedShape.setFill(Color.RED.getColor(Color.Intensity.I700)); + committedShape.setFill(Color.RED.getColor(Color.Intensity.I700)); + committedShape.setStroke(Color.RED.getColor(Color.Intensity.I700.next(2))); + } else { + notCommittedShape.setFill(newColor.getPaintColor()); + committedShape.setFill(newColor.getPaintColor()); + committedShape.setStroke(newColor.getStrokeColor()); } - - committedShape.setFill(newColor.getPaintColor()); - committedShape.setStroke(newColor.getStrokeColor()); }; updateColorDelegates.add(updateColor); // Set the initial color - updateColor.accept(color.get()); + updateColor.accept(location.getColor()); // Update the color of the circle when the color of the location is updated - color.addListener((obs, old, newColor) -> updateColor.accept(newColor)); + location.colorProperty().addListener((obs, old, newColor) -> updateColor.accept(newColor)); + location.failingProperty().addListener(obs -> updateColor.accept(location.getColor())); } 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/MessagePresentation.java b/src/main/java/ecdar/presentations/MessagePresentation.java index 17cefdd0..6dd49d82 100644 --- a/src/main/java/ecdar/presentations/MessagePresentation.java +++ b/src/main/java/ecdar/presentations/MessagePresentation.java @@ -80,9 +80,9 @@ private void initializeNearLabel() { } if (openComponent[0] != null) { - if (!EcdarController.getActiveCanvasPresentation().getController().getActiveModelPresentation().getController().getModel().equals(openComponent[0])) { + if (!Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getActiveModelPresentation().getController().getModel().equals(openComponent[0])) { SelectHelper.elementsToBeSelected = FXCollections.observableArrayList(); - Ecdar.getPresentation().getController().setActiveModelPresentationForActiveCanvas(Ecdar.getComponentPresentationOfComponent(openComponent[0])); + Ecdar.getPresentation().getController().getEditorPresentation().getController().setActiveModelPresentationForActiveCanvas(Ecdar.getComponentPresentationOfComponent(openComponent[0])); } SelectHelper.clearSelectedElements(); 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 3a42f329..85b0790b 100644 --- a/src/main/java/ecdar/presentations/MultiSyncTagPresentation.java +++ b/src/main/java/ecdar/presentations/MultiSyncTagPresentation.java @@ -5,7 +5,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; @@ -47,7 +46,7 @@ public MultiSyncTagPresentation(GroupedEdge edge, Runnable updateIOStatusOfSubEd Platform.runLater(() -> { // Added to avoid NullPointer exception for location aware and component in getDragBounds method edge.getNails().stream().filter(n -> n.getPropertyType().equals(DisplayableEdge.PropertyType.SYNCHRONIZATION)).findFirst().ifPresent(this::setLocationAware); - setComponent(EcdarController.getActiveCanvasPresentation().getController().activeComponentPresentation.getController().getComponent()); + setComponent(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().activeComponentPresentation.getController().getComponent()); updateTopBorder(); initializeMouseTransparency(); @@ -166,7 +165,7 @@ private void updateTopBorder() { for (Node child : controller.syncList.getChildren()) { ((SyncTextFieldPresentation) child).getController().textField - .setOnKeyPressed(EcdarController.getActiveCanvasPresentation().getController() + .setOnKeyPressed(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController() .getLeaveTextAreaKeyHandler((keyEvent) -> { if (keyEvent.getCode().equals(KeyCode.ENTER)) { @@ -182,11 +181,11 @@ private void updateTopBorder() { } private void updateColorAndMouseShape() { - EcdarController.getActiveCanvasPresentation().getController().activeComponentPresentation.getController().getComponent().colorProperty().addListener((observable, oldValue, newValue) -> { + Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().activeComponentPresentation.getController().getComponent().colorProperty().addListener((observable, oldValue, newValue) -> { controller.frame.setBackground(new Background(new BackgroundFill(newValue.getPaintColor(), new CornerRadii(0), Insets.EMPTY))); }); - Color color = EcdarController.getActiveCanvasPresentation().getController().activeComponentPresentation.getController().getComponent().getColor().getPaintColor(); + Color color = Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().activeComponentPresentation.getController().getComponent().getColor().getPaintColor(); controller.frame.setBackground(new Background(new BackgroundFill(color, new CornerRadii(0), Insets.EMPTY))); controller.frame.setCursor(Cursor.OPEN_HAND); @@ -215,8 +214,8 @@ private void initializeDragging() { event.consume(); Platform.runLater(() -> { - final double dragDistanceX = (event.getSceneX() - dragOffsetX.get()) / EcdarController.getActiveCanvasZoomFactor().get(); - final double dragDistanceY = (event.getSceneY() - dragOffsetY.get()) / EcdarController.getActiveCanvasZoomFactor().get(); + final double dragDistanceX = (event.getSceneX() - dragOffsetX.get()) / Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get(); + final double dragDistanceY = (event.getSceneY() - dragOffsetY.get()) / Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get(); double draggableNewX = getDragBounds().trimX(draggablePreviousX.get() + dragDistanceX); double draggableNewY = getDragBounds().trimY(draggablePreviousY.get() + dragDistanceY); diff --git a/src/main/java/ecdar/presentations/NailPresentation.java b/src/main/java/ecdar/presentations/NailPresentation.java index f15f1c22..f6ee250e 100644 --- a/src/main/java/ecdar/presentations/NailPresentation.java +++ b/src/main/java/ecdar/presentations/NailPresentation.java @@ -13,6 +13,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; @@ -52,6 +53,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); @@ -75,7 +77,6 @@ private void initializeRadius() { radiusUpdater.accept(controller.getNail().getPropertyType()); } - private void initializePropertyTag() { final TagPresentation propertyTag = controller.propertyTag; final Line propertyTagLine = controller.propertyTagLine; @@ -97,9 +98,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)) { @@ -182,10 +188,38 @@ private void updateSyncLabel(final TagPresentation propertyTag) { } private void initializeNailCircleColor() { + // When the color of the component updates, update the nail indicator as well + controller.getComponent().colorProperty().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 = () -> { + controller.nailCircle.setFill(Color.RED.getColor(Color.Intensity.I700)); + controller.nailCircle.setStroke(Color.RED.getColor(Color.Intensity.I700.next(2))); + }; + if (isFailing) { + updateNailColorOnFailingUpdate.run(); + } else { + updateNailColor(); + } + } + + private void updateNailColor() { final Runnable updateNailColor = () -> { final EnabledColor color = controller.getComponent().getColor(); - if(!controller.getNail().getPropertyType().equals(Edge.PropertyType.NONE)) { + if (controller.getEdge().getFailing() && controller.getNail().getPropertyType().equals(Edge.PropertyType.SYNCHRONIZATION)) { + controller.nailCircle.setFill(Color.RED.getColor(Color.Intensity.I700)); + controller.nailCircle.setStroke(Color.RED.getColor(Color.Intensity.I700.next(2))); + } + //If edge is not NONE + else if(!controller.getNail().getPropertyType().equals(Edge.PropertyType.NONE)) { controller.nailCircle.setFill(color.getPaintColor()); controller.nailCircle.setStroke(color.getStrokeColor()); } else { @@ -234,15 +268,7 @@ public void select() { @Override public void deselect() { - EnabledColor color = EnabledColor.getDefault(); - - // Set the color - if(!controller.getNail().getPropertyType().equals(Edge.PropertyType.NONE)) { - color = controller.getComponent().getColor(); - } - - controller.nailCircle.setFill(color.getPaintColor()); - controller.nailCircle.setStroke(color.getStrokeColor()); + 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..48576fd7 --- /dev/null +++ b/src/main/java/ecdar/presentations/ProcessPresentation.java @@ -0,0 +1,223 @@ +package ecdar.presentations; + +import ecdar.abstractions.Component; +import ecdar.controllers.ProcessController; +import ecdar.utility.colors.EnabledColor; +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.Consumer; + +/** + * The presentation of a Process which is shown in {@link SimulationPresentation}.
+ * 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); + + // 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 Consumer updateColor = (newColor) -> { + // 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(CORNER_SIZE); + controller.topLeftLine.setStartY(0); + controller.topLeftLine.setEndX(0); + controller.topLeftLine.setEndY(CORNER_SIZE); + controller.topLeftLine.setStroke(newColor.getStrokeColor()); + 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.getStrokeColor(), + BorderStrokeStyle.SOLID, + CornerRadii.EMPTY, + new BorderWidths(1), + Insets.EMPTY + ))); + }; + component.colorProperty().addListener(observable -> { + updateColor.accept(component.getColor()); + }); + updateColor.accept(component.getColor()); + } + + /** + * 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().setIntensity(2).getPaintColor()); + final Consumer updateColor = (newColor) -> { + // Set the background color to the lightest possible version of the color + controller.background.setFill(newColor.setIntensity(2).getPaintColor()); + }; + component.colorProperty().addListener(observable -> { + updateColor.accept(component.getColor()); + }); + + updateColor.accept(component.getColor()); + } + + /** + * 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 Consumer updateColor = (newColor) -> { + // Set the background of the toolbar + controller.toolbar.setBackground(new Background(new BackgroundFill( + newColor.getPaintColor(), + CornerRadii.EMPTY, + Insets.EMPTY + ))); + + // Set the icon color and rippler color of the toggleDeclarationButton + controller.toggleValuesButton.setRipplerFill(newColor.getTextColor()); + + controller.toolbar.setPrefHeight(TOOLBAR_HEIGHT); + controller.toggleValuesButton.setBackground(Background.EMPTY); + }; + controller.getComponent().colorProperty().addListener(observable -> updateColor.accept(component.getColor())); + + updateColor.accept(component.getColor()); + + // 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 + public ProcessController getController() { + return controller; + } +} diff --git a/src/main/java/ecdar/presentations/QueryPresentation.java b/src/main/java/ecdar/presentations/QueryPresentation.java index 975a763d..9c7bd48a 100644 --- a/src/main/java/ecdar/presentations/QueryPresentation.java +++ b/src/main/java/ecdar/presentations/QueryPresentation.java @@ -7,6 +7,7 @@ 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; @@ -17,6 +18,7 @@ import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCode; import javafx.scene.layout.*; +import javafx.geometry.Point2D; import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.material.Material; @@ -67,9 +69,9 @@ private void initializeTextFields() { controller.getQuery().commentProperty().bind(commentTextField.textProperty()); - queryTextField.setOnKeyPressed(EcdarController.getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler(keyEvent -> { + queryTextField.setOnKeyPressed(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler(keyEvent -> { Platform.runLater(() -> { - if (keyEvent.getCode().equals(KeyCode.ENTER)) { + if (keyEvent.getCode().equals(KeyCode.ENTER) && controller.getQuery().getType() != null) { runQuery(); } }); @@ -83,7 +85,7 @@ private void initializeTextFields() { } }); - commentTextField.setOnKeyPressed(EcdarController.getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler()); + commentTextField.setOnKeyPressed(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler()); }); } @@ -192,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()); @@ -248,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()); }); }); @@ -274,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) { @@ -283,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() : "---"); diff --git a/src/main/java/ecdar/presentations/RightSimPanePresentation.java b/src/main/java/ecdar/presentations/RightSimPanePresentation.java new file mode 100755 index 00000000..b9eca0ba --- /dev/null +++ b/src/main/java/ecdar/presentations/RightSimPanePresentation.java @@ -0,0 +1,23 @@ +package ecdar.presentations; + +import ecdar.abstractions.Decision; +import ecdar.controllers.RightSimPaneController; +import javafx.scene.layout.*; + +import java.util.function.Consumer; + +/** + * Presentation class for the right pane in the simulator + */ +public class RightSimPanePresentation extends StackPane { + private final RightSimPaneController controller; + + public RightSimPanePresentation(Consumer onDecisionSelected) { + controller = new EcdarFXMLLoader().loadAndGetController("RightSimPanePresentation.fxml", this); + controller.setOnDecisionSelected(onDecisionSelected); + } + + public RightSimPaneController getController() { + return controller; + } +} diff --git a/src/main/java/ecdar/presentations/SignatureArrow.java b/src/main/java/ecdar/presentations/SignatureArrow.java index aadb32c3..fb2bfdfb 100644 --- a/src/main/java/ecdar/presentations/SignatureArrow.java +++ b/src/main/java/ecdar/presentations/SignatureArrow.java @@ -1,5 +1,6 @@ package ecdar.presentations; +import ecdar.Ecdar; import ecdar.abstractions.Component; import ecdar.abstractions.EdgeStatus; import ecdar.controllers.EcdarController; @@ -12,6 +13,8 @@ import javafx.scene.shape.MoveTo; import javafx.scene.shape.Path; +import java.util.Objects; + /*** * Creates input and output arrows, that can for example be used on the side of components to show its signature */ @@ -19,6 +22,11 @@ public class SignatureArrow extends Group implements Highlightable { private SignatureArrowController controller; + public String getSignatureArrowLabel() { + return controller.signatureArrowLabel.getText(); + } + + /*** * Create a new signature arrow with a label for an edge, and different look depending on whether it's * an input or output arrow @@ -75,14 +83,14 @@ private void drawArrow(final String edgeName, final EdgeStatus edgeStatus) { // Draw the arrow head separately, so it is for example possible to have a dashed line and solid head final Path arrowHead = controller.signatureArrowHeadPath; final MoveTo moveHead = new MoveTo(xValue + 50, yValue); - final LineTo lineHead1 = new LineTo(xValue + 35 , yValue-5); // Upper line in the arrow head - final LineTo lineHead2 = new LineTo(xValue + 35 , yValue+5); // Lower line in the arrow head + final LineTo lineHead1 = new LineTo(xValue + 35, yValue - 5); // Upper line in the arrow head + final LineTo lineHead2 = new LineTo(xValue + 35, yValue + 5); // Lower line in the arrow head arrowHead.getElements().addAll(moveHead, lineHead1, moveHead, lineHead2); arrowHead.setStrokeWidth(1.0); final double radius = 2.0; // Radius for the circle - if(edgeStatus == EdgeStatus.OUTPUT){ + if (edgeStatus == EdgeStatus.OUTPUT) { arrowLine.getStyleClass().add("dashed"); controller.arrowBox.setAlignment(Pos.CENTER_LEFT); controller.signatureArrowCircle.setCenterX(xValue - radius); // Place the circle partly on the line @@ -121,14 +129,14 @@ public void unhighlight() { // Color matching SignatureArrow red SignatureArrow matchingSignatureArrow; if (controller.getEdgeStatus().equals(EdgeStatus.INPUT)) { - matchingSignatureArrow = (SignatureArrow) EcdarController.getActiveCanvasPresentation().getController() + matchingSignatureArrow = (SignatureArrow) Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController() .activeComponentPresentation.getController() .outputSignatureContainer.getChildren() .stream().filter(node -> node instanceof SignatureArrow && ((SignatureArrow) node).controller.getSyncText().equals(controller.getSyncText())) .findFirst().orElse(null); } else { - matchingSignatureArrow = (SignatureArrow) EcdarController.getActiveCanvasPresentation().getController() + matchingSignatureArrow = (SignatureArrow) Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController() .activeComponentPresentation.getController() .inputSignatureContainer.getChildren() .stream().filter(node -> node instanceof SignatureArrow && ((SignatureArrow) node).controller.getSyncText().equals(controller.getSyncText())) @@ -143,6 +151,12 @@ public void unhighlight() { Color.Intensity intensity = Color.Intensity.I800; this.colorArrowComponents(color, intensity); + + for (String s : controller.getComponent().getFailingIOStrings()) { + if (Objects.equals(getSignatureArrowLabel(), s) && controller.getComponent().getIsFailing()) { + this.recolorToRed(); + } + } } /** @@ -154,7 +168,13 @@ public void recolorToRed() { this.colorArrowComponents(color, intensity); } - /*** + public void recolorToGray() { + Color color = Color.GREY; + Color.Intensity intensity = Color.Intensity.I800; + this.colorArrowComponents(color, intensity); + } + + /** * Colors the components of the arrow * @param color The Color to color the components * @param intensity The Intensity of the color diff --git a/src/main/java/ecdar/presentations/SimEdgePresentation.java b/src/main/java/ecdar/presentations/SimEdgePresentation.java new file mode 100644 index 00000000..6cf47d9c --- /dev/null +++ b/src/main/java/ecdar/presentations/SimEdgePresentation.java @@ -0,0 +1,42 @@ +package ecdar.presentations; + +import ecdar.abstractions.Component; +import ecdar.abstractions.Edge; +import ecdar.controllers.SimEdgeController; +import ecdar.controllers.SimulationController; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.Group; + +/** + * The presentation class for the edges shown in the {@link SimulationPresentation} + */ +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 cursor should change to hand + this.setOnMouseEntered(event -> { + // ToDo NIELS: Handle change of functionality and utilization of ComponentInstances + if (controller.getEdge().isHighlighted()) + this.getScene().setCursor(javafx.scene.Cursor.HAND); + }); + this.setOnMouseExited(event -> this.getScene().setCursor(javafx.scene.Cursor.DEFAULT)); + } + + 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..7c127d56 --- /dev/null +++ b/src/main/java/ecdar/presentations/SimLocationPresentation.java @@ -0,0 +1,500 @@ +package ecdar.presentations; + +import ecdar.abstractions.Component; +import ecdar.abstractions.Location; +import ecdar.controllers.SimLocationController; +import ecdar.controllers.SimulationController; +import ecdar.utility.Highlightable; +import ecdar.utility.colors.EnabledColor; +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 SimulationPresentation}. + * 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(); + + // Delegate to style the label based on the color of the location + final Consumer updateColor = (newColor) -> { + idLabel.setTextFill(newColor.getTextColor()); + ds.setColor(newColor.getPaintColor()); + }; + + updateColorDelegates.add(updateColor); + + // Set the initial color + updateColor.accept(color.get()); + + // Update the color of the circle when the color of the location is updated + color.addListener((obs, old, newColor) -> updateColor.accept(newColor)); + } + + /** + * 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(), true); + controller.invariantTag.bindToColor(location.colorProperty(), 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(); + + // Delegate to style the label based on the color of the location + final Consumer updateColor = (newColor) -> { + circle.setFill(newColor.getPaintColor()); + circle.setStroke(newColor.getStrokeColor()); + }; + + updateColorDelegates.add(updateColor); + + // Set the initial color + updateColor.accept(color.get()); + + // Update the color of the circle when the color of the location is updated + color.addListener((obs, old, newColor) -> updateColor.accept(newColor)); + } + + /** + * 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(); + + // Delegate to style the label based on the color of the location + final Consumer updateColor = (newColor) -> { + notCommittedShape.setFill(newColor.getPaintColor()); + notCommittedShape.setStroke(newColor.getStrokeColor()); + }; + + updateColorDelegates.add(updateColor); + + // Set the initial color + updateColor.accept(color.get()); + + // Update the color of the circle when the color of the location is updated + color.addListener((obs, old, newColor) -> updateColor.accept(newColor)); + } + + /** + * 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(new EnabledColor(SelectHelper.SELECT_COLOR, SelectHelper.SELECT_COLOR_INTENSITY_NORMAL))); + } + + @Override + public void unhighlight() { + updateColorDelegates.forEach(colorConsumer -> { + colorConsumer.accept(controller.getLocation().getColor()); + }); + } +} \ 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..14bd2bfd --- /dev/null +++ b/src/main/java/ecdar/presentations/SimNailPresentation.java @@ -0,0 +1,263 @@ +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.colors.EnabledColor; +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 SimulationPresentation}
+ * 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()); + + // 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 EnabledColor color = controller.getComponent().getColor(); + + if(!controller.getNail().getPropertyType().equals(Edge.PropertyType.NONE)) { + controller.nailCircle.setFill(color.getPaintColor()); + controller.nailCircle.setStroke(color.getStrokeColor()); + } 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()); + + // 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))); + } + + @Override + public void highlightPurple() { + final Color color1 = Color.DEEP_PURPLE; + final Color.Intensity intensity = Color.Intensity.I500; + + // Set the color + controller.nailCircle.setFill(color1.getColor(intensity)); + controller.nailCircle.setStroke(color1.getColor(intensity.next(2))); + } + + /** + * Removes the highlight from the nail + */ + @Override + public void unhighlight() { + EnabledColor color = new EnabledColor(Color.GREY_BLUE, Color.Intensity.I800); + + // Set the color + if(!controller.getNail().getPropertyType().equals(Edge.PropertyType.NONE)) { + color = controller.getComponent().getColor(); + } + + controller.nailCircle.setFill(color.getPaintColor()); + controller.nailCircle.setStroke(color.getStrokeColor()); + } +} diff --git a/src/main/java/ecdar/presentations/SimTagPresentation.java b/src/main/java/ecdar/presentations/SimTagPresentation.java new file mode 100755 index 00000000..705144bc --- /dev/null +++ b/src/main/java/ecdar/presentations/SimTagPresentation.java @@ -0,0 +1,183 @@ +package ecdar.presentations; + +import ecdar.Ecdar; +import ecdar.abstractions.Component; +import ecdar.utility.colors.Color; +import ecdar.utility.colors.EnabledColor; +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.Consumer; + +/** + * The presentation for the tag shown on a {@link SimEdgePresentation} in the {@link SimulationPresentation}
+ * 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; + + /** + * 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 = Ecdar.CANVAS_PADDING * 2 - (newWidth % (Ecdar.CANVAS_PADDING * 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(TagPresentation.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 = TagPresentation.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) { + bindToColor(color, false); + } + + public void bindToColor(final ObjectProperty color, final boolean doColorBackground) { + final Consumer recolor = (newColor) -> { + if (doColorBackground) { + final Path shape = (Path) lookup("#shape"); + shape.setFill(newColor.nextIntensity(-1).getPaintColor()); + shape.setStroke(newColor.getStrokeColor()); + } + }; + + color.addListener(observable -> recolor.accept(color.get())); + recolor.accept(color.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..dc2b5ba3 --- /dev/null +++ b/src/main/java/ecdar/presentations/SimulationInitializationDialogPresentation.java @@ -0,0 +1,18 @@ +package ecdar.presentations; + +import com.jfoenix.controls.JFXDialog; +import ecdar.presentations.EcdarFXMLLoader; +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/SimulationPresentation.java b/src/main/java/ecdar/presentations/SimulationPresentation.java new file mode 100755 index 00000000..c9ef5f8b --- /dev/null +++ b/src/main/java/ecdar/presentations/SimulationPresentation.java @@ -0,0 +1,21 @@ +package ecdar.presentations; + +import ecdar.controllers.SimulationController; +import javafx.scene.layout.StackPane; + +public class SimulationPresentation extends StackPane { + private final SimulationController controller; + + public SimulationPresentation(String composition) { + controller = new EcdarFXMLLoader().loadAndGetController("SimulationPresentation.fxml", this); + controller.resetSimulation(composition); + } + + /** + * The way to get the associated/linked controller of this presenter + * @return the controller linked to this presenter + */ + public SimulationController getController() { + return controller; + } +} diff --git a/src/main/java/ecdar/presentations/StatePresentation.java b/src/main/java/ecdar/presentations/StatePresentation.java new file mode 100755 index 00000000..79877fd9 --- /dev/null +++ b/src/main/java/ecdar/presentations/StatePresentation.java @@ -0,0 +1,103 @@ +package ecdar.presentations; + +import com.jfoenix.controls.JFXRippler; +import ecdar.abstractions.State; +import ecdar.controllers.StateController; +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 state view element. + * It represents a single state within the trace of the simulation + */ +public class StatePresentation extends AnchorPane { + private final StateController controller; + private FadeTransition transition; + + public StatePresentation() { + controller = new EcdarFXMLLoader().loadAndGetController("StatePresentation.fxml", this); + + initializeRippler(); + initializeColors(); + initializeFadeAnimation(); + } + + public StatePresentation(State state) { + this(); + controller.setState(state); + } + + /** + * 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 StateController getController() { + return controller; + } +} diff --git a/src/main/java/ecdar/presentations/SyncTextFieldPresentation.java b/src/main/java/ecdar/presentations/SyncTextFieldPresentation.java index a4e7b82c..e957cc89 100644 --- a/src/main/java/ecdar/presentations/SyncTextFieldPresentation.java +++ b/src/main/java/ecdar/presentations/SyncTextFieldPresentation.java @@ -1,5 +1,6 @@ package ecdar.presentations; +import ecdar.Ecdar; import ecdar.abstractions.Edge; import ecdar.controllers.EcdarController; import ecdar.controllers.SyncTextFieldController; @@ -22,7 +23,7 @@ public SyncTextFieldPresentation(String placeholder, Edge edge) { controller.textField.setTranslateY(2); } else { controller.textField.setTranslateY(0); - EcdarController.getActiveCanvasPresentation().getController().leaveTextAreas(); + Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().leaveTextAreas(); } }); diff --git a/src/main/java/ecdar/presentations/SystemEdgePresentation.java b/src/main/java/ecdar/presentations/SystemEdgePresentation.java index 1d9b59f0..1abaf1e0 100644 --- a/src/main/java/ecdar/presentations/SystemEdgePresentation.java +++ b/src/main/java/ecdar/presentations/SystemEdgePresentation.java @@ -51,8 +51,8 @@ private void initializeBinding(final SystemEdge edge) { link.startYProperty().bind(edge.getTempNode().getEdgeY()); // Bind to mouse position - link.endXProperty().bind(EcdarController.getActiveCanvasPresentation().mouseTracker.xProperty().subtract(Ecdar.CANVAS_PADDING / 2)); - link.endYProperty().bind(EcdarController.getActiveCanvasPresentation().mouseTracker.xProperty().subtract(Ecdar.CANVAS_PADDING / 2)); + link.endXProperty().bind(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().mouseTracker.xProperty().subtract(Ecdar.CANVAS_PADDING / 2)); + link.endYProperty().bind(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().mouseTracker.xProperty().subtract(Ecdar.CANVAS_PADDING / 2)); } // If edge source and target changes, bind diff --git a/src/main/java/ecdar/presentations/SystemRootPresentation.java b/src/main/java/ecdar/presentations/SystemRootPresentation.java index 1861a94a..b978f410 100644 --- a/src/main/java/ecdar/presentations/SystemRootPresentation.java +++ b/src/main/java/ecdar/presentations/SystemRootPresentation.java @@ -4,6 +4,7 @@ import ecdar.abstractions.SystemRoot; import ecdar.controllers.SystemRootController; import ecdar.utility.Highlightable; +import ecdar.utility.colors.Color; import ecdar.utility.colors.EnabledColor; import ecdar.utility.helpers.ItemDragHelper; import ecdar.utility.helpers.SelectHelper; @@ -140,4 +141,9 @@ public void highlight() { public void unhighlight() { dyeFromSystemColor(); } + + @Override + public void highlightPurple() { + dye(new EnabledColor(Color.DEEP_PURPLE, SelectHelper.SELECT_COLOR_INTENSITY_NORMAL)); + } } diff --git a/src/main/java/ecdar/presentations/TagPresentation.java b/src/main/java/ecdar/presentations/TagPresentation.java index 9d11e518..663954b9 100644 --- a/src/main/java/ecdar/presentations/TagPresentation.java +++ b/src/main/java/ecdar/presentations/TagPresentation.java @@ -1,5 +1,6 @@ package ecdar.presentations; +import ecdar.Ecdar; import ecdar.abstractions.Component; import ecdar.controllers.EcdarController; import ecdar.utility.UndoRedoStack; @@ -9,6 +10,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.*; @@ -39,7 +41,7 @@ public class TagPresentation extends StackPane { LineTo l3; boolean hadInitialFocus = false; - static double TAG_HEIGHT = 16; + public static double TAG_HEIGHT = 16; public TagPresentation() { new EcdarFXMLLoader().loadAndGetController("TagPresentation.fxml", this); @@ -70,7 +72,7 @@ void initializeTextFocusHandler() { textFieldFocusProperty().addListener((observable, oldValue, newValue) -> { if (!hadInitialFocus && newValue) { hadInitialFocus = true; - EcdarController.getActiveCanvasPresentation().getController().leaveTextAreas(); + Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().leaveTextAreas(); } }); }); @@ -120,7 +122,9 @@ private void initializeShape() { }); // When enter or escape is pressed release focus - textField.setOnKeyPressed(EcdarController.getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler()); + Platform.runLater(() -> { + textField.setOnKeyPressed(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().getLeaveTextAreaKeyHandler()); + }); } private void initializeDragging(JFXTextField textField, Path shape) { @@ -146,8 +150,8 @@ private void initializeDragging(JFXTextField textField, Path shape) { shape.addEventHandler(MouseEvent.MOUSE_DRAGGED, event -> { event.consume(); - final double dragDistanceX = (event.getSceneX() - dragOffsetX.get()) / EcdarController.getActiveCanvasZoomFactor().get(); - final double dragDistanceY = (event.getSceneY() - dragOffsetY.get()) / EcdarController.getActiveCanvasZoomFactor().get(); + final double dragDistanceX = (event.getSceneX() - dragOffsetX.get()) / Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get(); + final double dragDistanceY = (event.getSceneY() - dragOffsetY.get()) / Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get(); double draggableNewX = getDragBounds().trimX(draggablePreviousX.get() + dragDistanceX); double draggableNewY = getDragBounds().trimY(draggablePreviousY.get() + dragDistanceY); @@ -280,6 +284,13 @@ 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/TracePanePresentation.java b/src/main/java/ecdar/presentations/TracePanePresentation.java new file mode 100755 index 00000000..605263c4 --- /dev/null +++ b/src/main/java/ecdar/presentations/TracePanePresentation.java @@ -0,0 +1,19 @@ +package ecdar.presentations; + +import ecdar.controllers.TracePaneController; +import javafx.scene.layout.*; + +/** + * The presentation class for the trace element that can be inserted into the simulator panes + */ +public class TracePanePresentation extends VBox { + final private TracePaneController controller; + + public TracePanePresentation() { + controller = new EcdarFXMLLoader().loadAndGetController("TracePanePresentation.fxml", this); + } + + public TracePaneController getController() { + return controller; + } +} diff --git a/src/main/java/ecdar/utility/Highlightable.java b/src/main/java/ecdar/utility/Highlightable.java index 2195978c..3d699a29 100644 --- a/src/main/java/ecdar/utility/Highlightable.java +++ b/src/main/java/ecdar/utility/Highlightable.java @@ -7,4 +7,7 @@ public interface Highlightable { public void highlight(); public void unhighlight(); + default void highlightPurple(){ + highlight(); + } } diff --git a/src/main/java/ecdar/utility/colors/EnabledColor.java b/src/main/java/ecdar/utility/colors/EnabledColor.java index dbafc118..fc75b523 100644 --- a/src/main/java/ecdar/utility/colors/EnabledColor.java +++ b/src/main/java/ecdar/utility/colors/EnabledColor.java @@ -5,17 +5,16 @@ import java.util.ArrayList; 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 ff792824..a8068ff3 100644 --- a/src/main/java/ecdar/utility/helpers/BindingHelper.java +++ b/src/main/java/ecdar/utility/helpers/BindingHelper.java @@ -1,10 +1,11 @@ package ecdar.utility.helpers; -import ecdar.controllers.EcdarController; +import ecdar.Ecdar; import ecdar.model_canvas.arrow_heads.ArrowHead; 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,9 +25,18 @@ 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.xProperty().subtract(x)); - subject.yProperty().bind(EcdarController.getActiveCanvasPresentation().mouseTracker.yProperty().subtract(y)); + subject.xProperty().bind(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().mouseTracker.xProperty().subtract(x)); + subject.yProperty().bind(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().mouseTracker.yProperty().subtract(y)); } public static void bind(final Link lineSubject, final ArrowHead arrowHeadSubject, final Circular source, final Circular target) { @@ -65,7 +75,7 @@ public static void bind(final Link subject, final Circle source, final MouseTrac public static void bind(final Link subject, final Circular source, final ObservableDoubleValue x, final ObservableDoubleValue y) { // Calculate the bindings (so that the line will be based on the circle circumference instead of in its center) - final LineBinding lineBinding = LineBinding.getCircularBindings(source, EcdarController.getActiveCanvasPresentation().mouseTracker, x, y); + final LineBinding lineBinding = LineBinding.getCircularBindings(source, Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().mouseTracker, x, y); // Bind the subjects properties accordingly to our calculations subject.startXProperty().bind(lineBinding.startX); @@ -131,7 +141,7 @@ protected double computeValue() { public static void bind(final ArrowHead subject, final Circular source, final ObservableDoubleValue x, final ObservableDoubleValue y) { // Calculate the bindings (so that the line will be based on the circle circumference instead of in its center) - final LineBinding lineBinding = LineBinding.getCircularBindings(source, EcdarController.getActiveCanvasPresentation().mouseTracker, x, y); + final LineBinding lineBinding = LineBinding.getCircularBindings(source, Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().mouseTracker, x, y); ObservableDoubleValue startX = lineBinding.startX; ObservableDoubleValue startY = lineBinding.startY; diff --git a/src/main/java/ecdar/utility/helpers/ClockConstraintsHandler.java b/src/main/java/ecdar/utility/helpers/ClockConstraintsHandler.java new file mode 100644 index 00000000..b865bb6f --- /dev/null +++ b/src/main/java/ecdar/utility/helpers/ClockConstraintsHandler.java @@ -0,0 +1,107 @@ +package ecdar.utility.helpers; + +import ecdar.abstractions.ClockConstraint; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class ClockConstraintsHandler { + /** + * Generates a string representation of the clock constraints of the provided state + * + * @param clockConstraints list of the clock constraints represent in the string + * @return a prettified string representation of the clock constraints + */ + public String getStateClockConstraintsString(List clockConstraints) { + if (clockConstraints.isEmpty()) return ""; + + StringBuilder clockConstraintsString = new StringBuilder(); + List mergedConstraints = new ArrayList<>(); + + for (int i = 0; i < clockConstraints.size(); i++) { + if (mergedConstraints.contains(i)) continue; // 'i' has already been merged + + for (int j = 1; j < clockConstraints.size(); j++) { + if (i == j || mergedConstraints.contains(j)) continue; // 'j' has already been merged + + ClockConstraint c1 = clockConstraints.get(i); + ClockConstraint c2 = clockConstraints.get(j); + + if (constraintClocksMatch(c1, c2)) { + clockConstraintsString.append(getMergedConstraintString(c1, c2)).append("\n"); + mergedConstraints.add(i); + mergedConstraints.add(j); + } + } + } + + for (int i = 0; i < clockConstraints.size(); i++) { + if (!mergedConstraints.contains(i)) { + clockConstraintsString.append(clockConstraints.get(i).toString()).append("\n"); + } + } + + // Remove trailing newline + clockConstraintsString.setLength(clockConstraintsString.length() - 1); + + return clockConstraintsString.toString(); + } + + /** + * Merges two clock constraints into a combined string. + * If the two constraints are incompatible, this returns the two individual string representations split by `\n` + * + * @param c1 first clock constraint + * @param c2 second clock constraint + * @return string representation of the merged clock constraint if compatible, or the two separate representations otherwise + */ + private String getMergedConstraintString(ClockConstraint c1, ClockConstraint c2) { + StringBuilder mergedConstraint = new StringBuilder(); + + if ((c1.comparator == '<' && c2.comparator == '>') || + (c1.comparator == '>' && c2.comparator == '<')) { + var smallestConstraint = c1.comparator == '>' ? c1 : c2; + var largestConstraint = c1.equals(smallestConstraint) ? c2 : c1; + + // Lower bound + mergedConstraint.append(smallestConstraint.constant) + .append(" <") + .append(smallestConstraint.isStrict ? "= " : " "); + + // Clock relation + mergedConstraint.append(smallestConstraint.clocks.getKey()); + if (smallestConstraint.clocks.getValue() != null) { + mergedConstraint.append(" - ").append(smallestConstraint.clocks.getValue()); + } + mergedConstraint.append(" "); + + // Upper bound + mergedConstraint.append("<") + .append(largestConstraint.isStrict ? "= " : " ") + .append(largestConstraint.constant); + + return mergedConstraint.toString(); + } else if (Objects.equals(c1.clocks.getValue(), c2.clocks.getKey()) && + Objects.equals(c1.clocks.getKey(), c2.clocks.getValue())) { + if (c1.constant == 0 && c2.constant == 0) { + return mergedConstraint.append(c1.clocks.getKey()) + .append(" = ") + .append(c1.clocks.getValue()).toString(); + } + } + + return mergedConstraint.append(c1).append("\n").append(c2).toString(); + } + + /** + * Checks if the tro constraints can be merged + * + * @param c1 first clock constraint + * @param c2 second clock constraint + * @return true if the two clock constraints are compatible, false otherwise + */ + private boolean constraintClocksMatch(ClockConstraint c1, ClockConstraint c2) { + return (Objects.equals(c1.clocks.getValue(), c2.clocks.getValue()) && Objects.equals(c1.clocks.getKey(), c2.clocks.getKey())) || (Objects.equals(c1.clocks.getValue(), c2.clocks.getKey()) && Objects.equals(c1.clocks.getKey(), c2.clocks.getValue())); + } +} diff --git a/src/main/java/ecdar/utility/helpers/ItemDragHelper.java b/src/main/java/ecdar/utility/helpers/ItemDragHelper.java index b082ac9b..a103c8b9 100644 --- a/src/main/java/ecdar/utility/helpers/ItemDragHelper.java +++ b/src/main/java/ecdar/utility/helpers/ItemDragHelper.java @@ -1,5 +1,6 @@ package ecdar.utility.helpers; +import ecdar.Ecdar; import ecdar.controllers.EcdarController; import ecdar.controllers.EdgeController; import ecdar.presentations.ComponentOperatorPresentation; @@ -123,8 +124,8 @@ public static void makeDraggable(final Node mouseSubject, final Node draggable, final DragBounds dragBounds = getDragBounds.get(); - final double dragDistanceX = (event.getSceneX() - dragOffsetX.get()) / EcdarController.getActiveCanvasZoomFactor().get(); - final double dragDistanceY = (event.getSceneY() - dragOffsetY.get()) / EcdarController.getActiveCanvasZoomFactor().get(); + final double dragDistanceX = (event.getSceneX() - dragOffsetX.get()) / Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get(); + final double dragDistanceY = (event.getSceneY() - dragOffsetY.get()) / Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get(); double draggableNewX = dragBounds.trimX(draggablePreviousX.get() + dragDistanceX); double draggableNewY = dragBounds.trimY(draggablePreviousY.get() + dragDistanceY); diff --git a/src/main/java/ecdar/utility/helpers/MouseCircular.java b/src/main/java/ecdar/utility/helpers/MouseCircular.java index d1779988..ae13cd51 100644 --- a/src/main/java/ecdar/utility/helpers/MouseCircular.java +++ b/src/main/java/ecdar/utility/helpers/MouseCircular.java @@ -1,5 +1,6 @@ package ecdar.utility.helpers; +import ecdar.Ecdar; import ecdar.controllers.EcdarController; import ecdar.utility.mouse.MouseTracker; import javafx.beans.property.DoubleProperty; @@ -14,7 +15,7 @@ public class MouseCircular implements Circular { private final DoubleProperty originalMouseY = new SimpleDoubleProperty(0d); private final DoubleProperty radius = new SimpleDoubleProperty(10); private final SimpleDoubleProperty scale = new SimpleDoubleProperty(1d); - private final MouseTracker mouseTracker = EcdarController.getActiveCanvasPresentation().mouseTracker; + private final MouseTracker mouseTracker = Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().mouseTracker; public MouseCircular(Circular initLocation) { //Set the initial x and y coordinates of the circular @@ -29,20 +30,18 @@ public MouseCircular(Circular initLocation) { mouseTracker.registerOnMouseDraggedEventHandler(event -> updatePosition()); // If the component is dragged while we are drawing an edge, update the coordinates accordingly - EcdarController.getActiveCanvasPresentation().getController().modelPane.translateXProperty().addListener((observable, oldValue, newValue) -> originalX.set( - originalX.get() - (newValue.doubleValue() - oldValue.doubleValue()) / EcdarController.getActiveCanvasZoomFactor().get())); - EcdarController.getActiveCanvasPresentation().getController().modelPane.translateYProperty().addListener((observable, oldValue, newValue) -> originalY.set( - originalY.get() - (newValue.doubleValue() - oldValue.doubleValue()) / EcdarController.getActiveCanvasZoomFactor().get())); - - // ToDo NIELS: When the width or height of the scene is changed, the coordinates should be updated + Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().modelPane.translateXProperty().addListener((observable, oldValue, newValue) -> originalX.set( + originalX.get() - (newValue.doubleValue() - oldValue.doubleValue()) / Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get())); + Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().modelPane.translateYProperty().addListener((observable, oldValue, newValue) -> originalY.set( + originalY.get() - (newValue.doubleValue() - oldValue.doubleValue()) / Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get())); } private void updatePosition() { final double dragDistanceX = mouseTracker.xProperty().get() - originalMouseX.get(); final double dragDistanceY = mouseTracker.yProperty().get() - originalMouseY.get(); - x.set(originalX.get() + dragDistanceX / EcdarController.getActiveCanvasZoomFactor().get()); - y.set(originalY.get() + dragDistanceY / EcdarController.getActiveCanvasZoomFactor().get()); + x.set(originalX.get() + dragDistanceX / Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get()); + y.set(originalY.get() + dragDistanceY / Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get()); } @Override diff --git a/src/main/java/ecdar/utility/helpers/SelectHelper.java b/src/main/java/ecdar/utility/helpers/SelectHelper.java index 12225423..47e23f00 100644 --- a/src/main/java/ecdar/utility/helpers/SelectHelper.java +++ b/src/main/java/ecdar/utility/helpers/SelectHelper.java @@ -1,5 +1,6 @@ package ecdar.utility.helpers; +import ecdar.Ecdar; import ecdar.code_analysis.Nearable; import ecdar.controllers.EcdarController; import ecdar.utility.colors.Color; @@ -19,7 +20,7 @@ public class SelectHelper { public static void select(final ItemSelectable selectable) { - EcdarController.getActiveCanvasPresentation().getController().leaveTextAreas(); + Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController().leaveTextAreas(); // Check if the element is already selected if (selectedElements.contains(selectable)) return; diff --git a/src/main/java/ecdar/utility/helpers/SimulationHighlighter.java b/src/main/java/ecdar/utility/helpers/SimulationHighlighter.java new file mode 100644 index 00000000..72f09d6d --- /dev/null +++ b/src/main/java/ecdar/utility/helpers/SimulationHighlighter.java @@ -0,0 +1,145 @@ +package ecdar.utility.helpers; + +import EcdarProtoBuf.ObjectProtos; +import ecdar.abstractions.Simulation; +import ecdar.abstractions.State; +import ecdar.presentations.DecisionPresentation; +import ecdar.presentations.ProcessPresentation; +import ecdar.presentations.StatePresentation; +import javafx.application.Platform; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableMap; +import javafx.scene.Node; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class SimulationHighlighter { + private final ObservableMap componentNameProcessPresentationMap; + private final ObjectProperty simulation = new SimpleObjectProperty<>(); + private ArrayList temporarilyHighlightedLocations = new ArrayList<>(); + + public SimulationHighlighter(ObservableMap componentNameProcessPresentationMap, ObjectProperty simulation) { + this.componentNameProcessPresentationMap = componentNameProcessPresentationMap; + this.simulation.bind(simulation); + } + + private Simulation getActiveSimulation() { + return simulation.get(); + } + + private List getProcessPresentations() { + return new ArrayList<>(componentNameProcessPresentationMap.values()); + } + + /** + * Initializer method to set up listeners that handle highlighting when selected/current state/transition changes + */ + public void updateHighlighting() { + Platform.runLater(() -> { + unhighlightProcesses(); + highlightProcessState(getActiveSimulation().currentState.get()); + }); + } + + /** + * Unhighlights all processes + */ + public void unhighlightProcesses() { + for (final ProcessPresentation presentation : getProcessPresentations()) { + presentation.getController().unhighlightProcess(); + presentation.showActive(); + } + } + + /** + * Initializes the fading of states in the trace list when a state is previewed + */ + public void fadeSucceedingTraceStates(List traceListStates) { + var activeStatePresentation = traceListStates.stream() + .filter(n -> ((StatePresentation) n) + .getController().getState() + .equals(getActiveSimulation().currentState.get())) + .findFirst().orElse(null); + + if (activeStatePresentation == null) return; + + traceListStates.forEach(trace -> trace.setOpacity(1)); + + int i = 0; + while (i < traceListStates.size() && traceListStates.get(i) != activeStatePresentation) { + traceListStates.get(i).setOpacity(0.4); + i++; + } + } + + /** + * Highlights the edges from the reachability response ToDO NIELS: Trigger reachability highlighting + */ + public void highlightReachabilityEdges(ArrayList ids) throws NullPointerException { + //unhighlight all edges + for (var comp : getActiveSimulation().simulatedComponents) { + for (var edge : comp.getEdges()) { + edge.setIsHighlightedForReachability(false); + } + } + //highlight the edges from the reachability response + for (var comp : getActiveSimulation().simulatedComponents) { + for (var edge : comp.getEdges()) { + for (var id : ids) { + if (edge.getId().equals(id)) { + edge.setIsHighlightedForReachability(true); + } + } + } + } + } + + /** + * Finds the processes for the input locations in the input {@link State} and highlights the locations. + * + * @param state The state with the locations to highlight + */ + public void highlightProcessState(final State state) { + Platform.runLater(() -> state.getComponentLocationMap().forEach((comp, loc) -> { + componentNameProcessPresentationMap.get(comp) + .getController().highlightLocation(loc); + })); + } + + /** + * Highlights the edges involved in the action of a decision + * + * @param decision decision presentations to extract the action from + */ + public void highlightDecisionEdges(DecisionPresentation decision) { + List involvedEdgeIds = decision.getController() + .getDecision() + .protoDecision.getEdgesList() + .stream().map(ObjectProtos.Edge::getId) + .collect(Collectors.toList()); + + componentNameProcessPresentationMap.values().forEach(p -> p.getController() + .getComponent().getEdges() + .forEach(e -> e.setIsHighlighted(involvedEdgeIds.contains(e.getId())))); + } + + /** + * Removes highlighting from all edges + */ + public void unhighlightEdges() { + componentNameProcessPresentationMap.values().forEach(p -> p.getController() + .getComponent().getEdges() + .forEach(e -> e.setIsHighlighted(false))); + } + + + public void highlightPreview(final State state) { + Platform.runLater(() -> state.getComponentLocationMap().forEach((comp, loc) -> { + componentNameProcessPresentationMap.get(comp).getController().highlightLocation(loc); + temporarilyHighlightedLocations.add(loc); + })); + } +} 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..70e8d5a2 160000 --- a/src/main/proto +++ b/src/main/proto @@ -1 +1 @@ -Subproject commit 476f1afd62596ec6a841227a3caa5fd40abc9c6e +Subproject commit 70e8d5a2dca183687cc0827d4ade7441fbdb4ddf diff --git a/src/main/resources/ecdar/main.css b/src/main/resources/ecdar/main.css index 048a320a..16c63c23 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/DecisionPresentation.fxml b/src/main/resources/ecdar/presentations/DecisionPresentation.fxml new file mode 100755 index 00000000..2c330c84 --- /dev/null +++ b/src/main/resources/ecdar/presentations/DecisionPresentation.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/ecdar/presentations/EcdarPresentation.fxml b/src/main/resources/ecdar/presentations/EcdarPresentation.fxml index ad248d97..e6ebecc6 100644 --- a/src/main/resources/ecdar/presentations/EcdarPresentation.fxml +++ b/src/main/resources/ecdar/presentations/EcdarPresentation.fxml @@ -1,5 +1,8 @@ + + + @@ -7,8 +10,6 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
- - - + - - - + @@ -194,7 +122,7 @@ - + @@ -204,7 +132,8 @@ - + + @@ -244,6 +173,19 @@ + + + + + + + + + + + + @@ -291,8 +233,21 @@ - - + + + + + + + + + + + + + + + - - + + @@ -534,4 +489,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/EnginePresentation.fxml b/src/main/resources/ecdar/presentations/EnginePresentation.fxml index 1577e856..af686165 100644 --- a/src/main/resources/ecdar/presentations/EnginePresentation.fxml +++ b/src/main/resources/ecdar/presentations/EnginePresentation.fxml @@ -94,7 +94,10 @@ - Default + + Default + Thread Safe + \ No newline at end of file diff --git a/src/main/resources/ecdar/presentations/LeftSimPanePresentation.fxml b/src/main/resources/ecdar/presentations/LeftSimPanePresentation.fxml new file mode 100755 index 00000000..7035a34a --- /dev/null +++ b/src/main/resources/ecdar/presentations/LeftSimPanePresentation.fxml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + 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..9628aa27 --- /dev/null +++ b/src/main/resources/ecdar/presentations/ProcessPresentation.fxml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + + +
+
+
+ + + +
+
+ + + + + + +
+
+
\ 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 8f0cb331..57aa8c01 100644 --- a/src/main/resources/ecdar/presentations/QueryPresentation.fxml +++ b/src/main/resources/ecdar/presentations/QueryPresentation.fxml @@ -2,8 +2,6 @@ - - - + - + @@ -33,7 +31,7 @@ - + + + + + + + + + + + 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..332e082d --- /dev/null +++ b/src/main/resources/ecdar/presentations/SimulationInitializationDialogPresentation.fxml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + Simulation Initialization + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/ecdar/presentations/SimulationPresentation.fxml b/src/main/resources/ecdar/presentations/SimulationPresentation.fxml new file mode 100755 index 00000000..d3ab4bab --- /dev/null +++ b/src/main/resources/ecdar/presentations/SimulationPresentation.fxml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/ecdar/presentations/StatePresentation.fxml b/src/main/resources/ecdar/presentations/StatePresentation.fxml new file mode 100755 index 00000000..4b9a3481 --- /dev/null +++ b/src/main/resources/ecdar/presentations/StatePresentation.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/ecdar/presentations/TracePanePresentation.fxml b/src/main/resources/ecdar/presentations/TracePanePresentation.fxml new file mode 100755 index 00000000..929586eb --- /dev/null +++ b/src/main/resources/ecdar/presentations/TracePanePresentation.fxml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/backend/QueryTest.java b/src/test/java/ecdar/backend/QueryTest.java index 3b9a07ad..4f9bfbcd 100644 --- a/src/test/java/ecdar/backend/QueryTest.java +++ b/src/test/java/ecdar/backend/QueryTest.java @@ -8,11 +8,11 @@ public class QueryTest { @Test - public void testExecuteQuery() { + public void testExecutionOfQuery() { Engine e = mock(Engine.class); Query q = new Query("refinement: (Administration || Machine || Researcher) <= Spec", "", QueryState.UNKNOWN, e); q.execute(); - verify(e, times(1)).enqueueQuery(eq(q), any(), any()); + verify(e, times(1)).enqueueRequest(eq(q), any(), any()); } } 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 13f058df..bc543b3b 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 ecdar.utility.colors.EnabledColor; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; 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..19e63b4d --- /dev/null +++ b/src/test/java/ecdar/simulation/ReachabilityTest.java @@ -0,0 +1,161 @@ +package ecdar.simulation; + +import ecdar.Ecdar; +import ecdar.abstractions.Component; +import ecdar.abstractions.Location; +import ecdar.controllers.SimLocationController; +import ecdar.controllers.SimulationController; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +// ToDo NIELS: Fix tests +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"); + + List components = new ArrayList<>(); + components.add("C1"); + components.add("C2"); + components.add("C3"); + + var result = SimLocationController.getSimLocationReachableQuery(location, component, "query"); + + assertTrue(result.matches(regex)); + } + + @Test + void reachabilityQueryLocationPosition1TestSuccess() { + var location = new Location(); + location.setId("L1"); + var component = new Component(); + component.setName("C1"); + + List components = new ArrayList<>(); + components.add("C1"); + components.add("C2"); + components.add("C3"); + + var result = SimLocationController.getSimLocationReachableQuery(location, component, "query"); + + int indexOfOpeningBracket = result.indexOf('['); + int indexOfComma = 0; + + for (int i = 0; i < result.length(); i++) { + if (result.charAt(i) == ',') { + indexOfComma = i; + break; + } + } + + var output = result.substring(indexOfOpeningBracket + 1, indexOfComma); + assertEquals(output, location.getId()); + } + + @Test + void reachabilityQueryLocationPosition2TestSuccess() { + var location = new Location(); + location.setId("L123"); + var component = new Component(); + component.setName("C2"); + + List components = new ArrayList<>(); + components.add("C1"); + components.add("C2"); + components.add("C3"); + + var result = SimLocationController.getSimLocationReachableQuery(location, component, "query"); + + int indexOfFirstComma = 0; + int indexofSecondComma = 0; + boolean commaSeen = false; + + for (int i = 0; i < result.length(); i++) { + if (result.charAt(i) == ',') { + if(commaSeen){ + indexofSecondComma = i; + } + else { + indexOfFirstComma = i; + commaSeen = true; + } + } + } + + var output = result.substring(indexOfFirstComma + 1,indexofSecondComma); + assertEquals(output, location.getId()); + } + + @Test + void reachabilityQueryLocationPosition3TestSuccess() { + var location = new Location(); + location.setId("L12345"); + var component = new Component(); + component.setName("C3"); + + List components = new ArrayList<>(); + components.add("C1"); + components.add("C2"); + components.add("C3"); + + var query = SimLocationController.getSimLocationReachableQuery(location, component, "query"); + + int indexOfLastComma = 0; + + for (int i = query.length()-1; i > 0; i--) { + if (query.charAt(i) == ',') { + indexOfLastComma = i; + break; + } + } + + int indexOfClosingBracket = query.indexOf(']'); + + var output = query.substring(indexOfLastComma + 1,indexOfClosingBracket); + + assertEquals(output, location.getId()); + } + + @Test + void reachabilityQueryNumberOfLocationsTestSuccess() { + var location = new Location(); + location.setId("L1"); + var component = new Component(); + component.setName("C1"); + + List components = new ArrayList<>(); + components.add("C1"); + components.add("C2"); + components.add("C3"); + components.add("C4"); + + var query = SimLocationController.getSimLocationReachableQuery(location, component, "query"); + int commaCount = 0; + for (int i = 0; i < query.length(); i++) { + if (query.charAt(i) == ',') { + commaCount++; + } + } + + int expected = commaCount + 1; + +// assertEquals(expected, SimulationController.getSimulationHandler().getComponentsInSimulation().size()); + } +} diff --git a/src/test/java/ecdar/simulation/SimulationTest.java b/src/test/java/ecdar/simulation/SimulationTest.java new file mode 100644 index 00000000..fe13d070 --- /dev/null +++ b/src/test/java/ecdar/simulation/SimulationTest.java @@ -0,0 +1,116 @@ +package ecdar.simulation; + +import EcdarProtoBuf.EcdarBackendGrpc; +import EcdarProtoBuf.ObjectProtos; +import EcdarProtoBuf.QueryProtos; +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.Arrays; +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(); + + // TODO fix this test + @Test + public void testGetInitialStateHighlightsTheInitialLocation() { + final List components = generateComponentsWithInitialLocations(); + + BindableService testService = new EcdarBackendGrpc.EcdarBackendImplBase() { + @Override + public void startSimulation(QueryProtos.SimulationStartRequest request, + StreamObserver responseObserver) { + try { + var leafLocations = components.stream() + .map(c -> ObjectProtos.LeafLocation.newBuilder() + .setComponentInstance(ObjectProtos.ComponentInstance.newBuilder() + .setComponentName(c.getName())) + .setId(c.getInitialLocation().getId()) + .build()) + .collect(Collectors.toList()); + + var locationTreeBuilder = ObjectProtos.LocationTree.newBuilder(); + leafLocations.stream().map(locationTreeBuilder::mergeLeafLocation); + + var locationTree = locationTreeBuilder.build(); + + ObjectProtos.State state = ObjectProtos.State.newBuilder().setLocationTree(locationTree).build(); + ObjectProtos.Decision decision = ObjectProtos.Decision.newBuilder().setSource(state).build(); + QueryProtos.SimulationStepResponse response = QueryProtos.SimulationStepResponse.newBuilder() + .addNewDecisionPoints(decision) + .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.LocationTree[components.size()]; + + for (int i = 0; i < components.size(); i++) { + Component comp = components.get(i); + expectedResponse[i] = ObjectProtos.LocationTree.newBuilder() + .setLeafLocation(ObjectProtos.LeafLocation.newBuilder().setId(comp.getInitialLocation().getId()) + .setComponentInstance(ObjectProtos.ComponentInstance + .newBuilder() + .setComponentName(comp.getName()).build() + ) + ) + .build(); + } + + var result = stub.startSimulation(request).getFullState().getLocationTree(); + + Assertions.assertTrue(Arrays.asList(expectedResponse).contains(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 78fbb7e9..fd05dc52 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/ui/UiSwitcherTest.java b/src/test/java/ecdar/ui/UiSwitcherTest.java new file mode 100644 index 00000000..73f39b7f --- /dev/null +++ b/src/test/java/ecdar/ui/UiSwitcherTest.java @@ -0,0 +1,40 @@ +package ecdar.ui; + +import org.junit.jupiter.api.Test; +import org.testfx.util.WaitForAsyncUtils; + +import com.jfoenix.controls.JFXComboBox; + +import ecdar.Ecdar; +import ecdar.abstractions.Query; +import ecdar.abstractions.QueryState; +import ecdar.controllers.EcdarController; +import ecdar.controllers.EcdarController.Mode; +import javafx.application.Platform; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class UiSwitcherTest extends TestFXBase { + @Test + public void UiSwitcherNoComponentsTest() { + Ecdar.setUpForTest(); + WaitForAsyncUtils.waitForFxEvents(); + clickOn("#switchGuiView"); + WaitForAsyncUtils.waitForFxEvents(); + JFXComboBox simDialog = lookup("#simulationComboBox").query(); + WaitForAsyncUtils.waitForFxEvents(); + assertFalse(simDialog.isShowing()); + + WaitForAsyncUtils.waitForFxEvents(); + Platform.runLater(() -> Ecdar.getProject().getQueries().add(new Query("(Component1)", "null", QueryState.UNKNOWN))); + WaitForAsyncUtils.waitForFxEvents(); + clickOn("#switchGuiView"); + WaitForAsyncUtils.waitForFxEvents(); + JFXComboBox simDialogComboBox = lookup("#simulationComboBox").query(); + Platform.runLater(() -> simDialogComboBox.selectionModelProperty().get().select(0)); + WaitForAsyncUtils.waitForFxEvents(); + clickOn("#startButton"); + WaitForAsyncUtils.waitForFxEvents(); + assertEquals(EcdarController.currentMode.get(), Mode.Simulator); + } +} 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); + } +}