diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..30f97862
--- /dev/null
+++ b/.github/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+ecdar@cs.aau.dk.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
\ No newline at end of file
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 00000000..0f73619e
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,96 @@
+# Contributing
+When contributing to this repository, make your own fork and create pull requests to this repo from there.
+
+## Issues
+If you find a bug or a missing feature, feel free to create an issue. The system is continuously under development and suggestions are always welcome.
+
+### Bugs
+To increase the chances of the issue being resolved, please include the following (or use the `Bug Report` issue template):
+- Concise title that states the issue
+- An in-depth description of the issue
+ - What happened
+ - What was expected to happen
+ - Suggestion on possible cause [Not required]
+- Images (if relevant)
+
+### Feature
+To increase the chances of the issue being resolved, please include the following (or use the `Feature Request` issue template):
+- Concise title that describes the feature
+- An in-depth description of the feature
+ - What is the feature
+ - Use case
+ - How should it work for the user
+ - Suggestions for implementation [Not required]
+- Reasoning behind the request
+ - Added value
+ - Improved user experience
+
+> :information_source: To help organising the issue, please attach relevant tags (eg. `bug`, `feature`, etc.).
+
+## Pull Requests
+Pull requests are continuously being reviewed and merged. In order to ease this process, please open a pull request as draft, as long as it is under development, to notify anyone else that a given feature/issue is being worked on.
+
+Additionally, please add `Closes #{ISSUE_ID}` if the pull request is linked to a specific issue. If a PR addresses multiple pull requests, please add `Closes #{ISSUE_ID}` for each one.
+
+A CI workflow is executed against all pull requests and must succeed before the pull request can be merged, so please make sure that you check the workflow status and potential error messages.
+
+## Tests
+All non-UI tests are executed as part of the CI workflow and hence must succeed before merging. The tests are written with JUnit and relevant tests should be added when new code is added. If you are new to JUnit, you can check out syntax and structure [here](https://junit.org/junit5/docs/current/user-guide/).
+
+The test suite can be executed locally by running:
+```shell
+./gradlew test
+```
+
+> :information_source: Currently, the codebase has high coupling, which has made testing difficult and the test suite very small.
+
+### UI Tests
+For features that are highly coupled with the interface, a second test suite has been added under `src/test/java/ecdar/ui`. These tests are excluded from the `test` task are can be executed by running:
+```shell
+./gradlew uiTest
+```
+These tests are more intensive to run and utilizes a robot for interacting with a running process of the GUI. The tests are implemented using [TestFX](https://github.com/TestFX/TestFX). As these tests are more intensive, they are not run as part of the standard CI workflow.
+
+You should prefer writing non-UI tests, as they are less demanding and are part of the CI workflow.
+
+## Code Organisation
+The code within the project is structure based on the Model-View-ViewModel (**MVVM**) architectural pattern. However, the terms _Abstraction_, _Presentation_, and _Controller_ are used instead of _Model_, _View_, and _View-Model_ respectively.
+This means that each element in the system consists of:
+- An _Abstraction_ (located in `abstractions` package).
+- A _Controller_ (located in `controllers` package).
+- A _Presentation_ (located in `presentations` package).
+ - Most of the presentations are related to an `FXML` markup file that specifies the look of the presentation. These files are located in `src/main/resources/ecdar/presentations`.
+
+In addition to these, a `utility` package is used for additional business logic to improve separation of concern and enhance the testability of the system.
+
+### Abstractions
+The abstractions are used to represent logical elements, such as `components`, `locations`, and `edges`. These classes should mostly be pure data objects. They are used to save and load data to and from existing project files.
+
+### Controllers
+The controllers contain the business logic of the system. They function as the link between the UI and the abstractions.
+This is implemented such that an action performed to an element in the UI triggers a method inside the controller, which then alters the state of the related abstraction.
+
+They implement the `Initializable` interface and are initialized through their associated presentation when an instance of that is instantiated. Hierarchically, a presentation therefore contains a controller.
+
+Each controller controls an instance of its related abstraction. If an action to one element should affect another element, this effect is enforced through the controller.
+
+### Presentations
+As mentioned above, most of the presentations are split into a Java class and an FXML markup file. The Java class can be seen as a shell to initialize the FXML element from inside the business logic. It initializes the related controller and ensures that any needed elements are set within it. This allows the controllers to be initialized without any UI elements, which is very useful for testing, while ensuring that they are correctly connected while the UI is running.
+
+The FXML files are markup specifying how the elements should look in the UI and have a reference to the related controller. Each element that should be addressable or changeable from the controller has an `fx:id` that is directly referenced as a member inside the controller. The direct connection to a controller allows events, such as `onPressed`, to trigger the correct methods in the controller and also helps IDEs identified any potentially missing methods or members.
+
+> :question: **Why use both a controller and a presentation Java file?**\
+> The advantage of during this is that the `controller` can contain all the business logic and bindings to the FXML elements, while the `presentation` can be used to instantiate and reference the UI elements inside the Java code. The `controller` should contain the logic and is bound within the FXML file, so the `presentation` Java file should be seen as a shell.
+
+### Utility
+To increase the testability and separation of concern further, the `utility` package is introduced. This package includes useful functionality that is either used in multiple unrelated classes or outside the responsibility of the given class.
+
+An example of one of the classes located in this package is the `UndoRedoStack` used to keep track of actions performed by the user.
+
+### Miscellaneous
+Besides the packages mentioned above, some larger functionalities are located in their own packages. Here is a small description of each:
+- `backend`: Responsible for the communication with the engines and model checking.
+- `code_analysis`: Responsible for analysing the elements of the current project and construct messages if errors or warnings are encountered.
+- `issues`: Classes for representing `Errors`, `Issues`, and `Warnings`.
+- `model_canvas.arrow_heads`: Arrowheads used in the UI to visualize the direction of edges.
+- `mutation [Deprecrated]`: Functionality for supporting mutation testing of components. **This feature is currently not implemented in the engines and is therefore currently not supported**.
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..ab912f27
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,44 @@
+---
+name: Bug
+about: File a bug report
+title: '[BUG]
'
+labels: bug
+assignees: ''
+---
+
+
+
+### Description
+
+#### Where was the error encountered
+- [ ] Using a distribution from the main ECDAR repository
+
+
+- [ ] Using a local JDK
+ - **Used JDK** (ex. Zulu 11.62.17 JDK FX 11.0.18):
+ - **Operating System**:
+ - [ ] Linux
+ - [ ] MacOS Arm
+ - [ ] MacOS Intel
+ - [ ] Windows
+
+### Steps To Reproduce
+
+
+### Images
+
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..8e8d9712
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,25 @@
+---
+name: Feature
+about: Suggest a new feature
+title: '[Feature] '
+labels: enhancement
+assignees: ''
+---
+
+
+
+### Description
+
+
+### Reason For Request
+
\ No newline at end of file
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..4330290c
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,4 @@
+Closes #{ISSUE_ID}
+
+### Changes/Additions:
+-
\ No newline at end of file
diff --git a/README.md b/README.md
index 382e33fd..47f4663d 100644
--- a/README.md
+++ b/README.md
@@ -1,52 +1,84 @@
# Ecdar
-
Ecdar is an abbreviation of Environment for Compositional Design and Analysis of Real Time Systems.
-This repo contains the source code for the graphical user interface, in order to run queries you will need the
+This repo contains the source code for the graphical user interface. In order to run queries you will need the
j-ecdar and revaal executables.
+> :information_source: If the goal is to use ECDAR, please goto the [main ECDAR repository](https://github.com/Ecdar/ECDAR), which contains releases for all supported platforms. These releases contain all dependencies, including the engines and a JRE.
+
+## Screenshots
+| | |
+|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|
+
+## H-UPPAAL
+This project is a hard fork of https://github.com/ulriknyman/H-Uppaal.
+
+
## Dependencies
-This repository utilizes the Ecdar-Proto repository for structuring the communication between the GUI and the engines. This dependency is implemented as a submodule which needs to be pulled and updated. If you have not yet cloned the code from this repository (the GUI), you can clone both the GUI and the submodule containing the Proto repository by running the following command:
+This section covers what dependencies are currently needed by the GUI.
+
+### JVM
+As with all Java applications, a working JVM is required to run the project.
+
+You will need Java version 11 containing JavaFX. We suggest downloading Azul's Java 11 from https://www.azul.com/downloads/?version=java-11-lts&package=jdk-fx, as this is the version used by the main development team.
-``` sh
+### Ecdar-ProtoBuf
+This repository utilizes the [Ecdar-ProtoBuf repository](https://github.com/Ecdar/Ecdar-ProtoBuf) for the communication with the engines. This dependency is implemented as a submodule that needs to be pulled and updated. If you have not yet cloned the code from this repository (the GUI), you can clone both the GUI and the submodule containing the ProtoBuf repository by running the following command:
+
+```shell
git clone --recurse-submodules git@github.com:Ecdar/Ecdar-GUI.git
```
-If you have already cloned this repository, you can clone the Proto submodule by running the following command, from a terminal inside the GUI repository directory:
+If you have already cloned this repository, you can clone the ProtoBuf submodule by running the following command from a terminal in the GUI repository directory:
-``` sh
+```shell
git submodule update --init --recursive
```
-## How to Run
-You will need a working JVM verion 11 with java FX in order to run the GUI. We suggest downloading from https://www.azul.com/downloads/?version=java-11-lts&package=jdk-fx.
+### Engines (needed for model-checking)
+In order to use the model-checking capabilities of the system, it is necessary to download at least one engine for the used operating system and place it in the `lib` directory.
+
+> :information_source: The latest version of each engine can be downloaded from:
+> * https://github.com/Ecdar/j-Ecdar
+> * https://github.com/Ecdar/Reveaal
-To run the gui use the gradle wrapper script
+The engines can then be configured in the GUI as described in [Engine Configuration](#engine_configuration).
-``` sh
-./gradlew(.bat) run
+## How to Run
+After having retrieved the code and acquired all the dependencies mentioned in [Dependencies](#dependencies), the GUI can be started using the following command:
+```shell
+./gradlew run
```
+> :information_source: All Gradle commands in this document are Unix specific, for Windows users, replace `./gradlew` with `./gradlew.bat`.
+
+
## Engine Configuration
-Download the latest version of the engine from:
+In order to utilize the model-checking capabilities of the system, at least one engine must be configured.
+The distributions available at [ECDAR](https://github.com/Ecdar/ECDAR) will automatically load the default engines on startup, but this is currently not working when running the GUI through Gradle.
+For the same reason, the `Reset Engines` button will clear the engines but will not be able to load the packaged once.
- * https://github.com/Ecdar/j-Ecdar
- * https://github.com/Ecdar/Reveaal
+An engine can be added through the configurator found under `Options > Engines Options` in the menubar, which opens the pop-up shown below.
-Unpack and move the downloaded files to the `lib` folder. You can also configure custom engine locations from the GUI.
+
+> :information_source: If you accidentally removed or changed an engine, these changes can be reverted be pressing `Cancel` or by clicking outside the pop-up. Consequently, if any changes should be saved, **MAKE SURE TO PRESS `Save`**
-## Screenshots
+### Address
+The _Address_ is either the address of a server running the engine (for remote execution) or a path to a local engine binary (for this, the _Local_ checkbox must be checked).
-| | |
-|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|
+### Port range
+The GUI uses gRPC for the communication with the engines and will therefore need at least one free port. This range directly limits the number of instances of the engine that will be started.
+> :warning: Make sure AT LEAST one port is free within the specified range. For instance, the default port range for Reveaal is _5032_ - _5040_.
-Sample Projects
-----
-See sample projects in the `samples` folder.
+### Default
+If an engine is marked with _Default_, all added queries will be assigned that engine.
-H-UPPAAL
-----------
-This project is a hard fork of https://github.com/ulriknyman/H-Uppaal.
+## Exemplary Projects
+To get started and get an idea of what the system can be used for, multiple examples can be found in the `examples` directory.
+These projects include preconfigured models and queries to execute against them.
+For the theoretical background and what the tool can be used for, please check out the latest research links at [here](https://ulrik.blog.aau.dk/ecdar/).
+# Contributing
+If you are interested in contributing to the project, please read the [contributing](.github/CONTRIBUTING.md) file. Here you will find guides on how to create issues and commit changes to the repository.
\ No newline at end of file
diff --git a/examples/AGTest/Queries.json b/examples/AGTest/Queries.json
index f367b70a..89fff7bd 100644
--- a/examples/AGTest/Queries.json
+++ b/examples/AGTest/Queries.json
@@ -5,7 +5,7 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
},
{
"query": "refinement: ((A1 || G2) \\ A2) \u003c\u003d G1",
@@ -13,7 +13,7 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
},
{
"query": "refinement: ((A1 || G1) \\ A2) \u003c\u003d Imp",
@@ -21,6 +21,6 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
}
]
\ No newline at end of file
diff --git a/examples/CarAlarm/Model/Queries.json b/examples/CarAlarm/Model/Queries.json
index d9a20d12..b5164aa2 100644
--- a/examples/CarAlarm/Model/Queries.json
+++ b/examples/CarAlarm/Model/Queries.json
@@ -5,7 +5,7 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
},
{
"query": "specification: Alarm",
@@ -13,7 +13,7 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
},
{
"query": "reachability: Alarm.L11",
@@ -21,7 +21,7 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
},
{
"query": "reachability: Alarm.L12",
@@ -29,7 +29,7 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
},
{
"query": "reachability: Alarm.L9",
@@ -37,7 +37,7 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
},
{
"query": "specification: (Alarm: A[] not Alarm.L7 || Alarm.x\u003c\u003d30)",
@@ -45,6 +45,6 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
}
]
diff --git a/examples/EcdarUniversity/Queries.json b/examples/EcdarUniversity/Queries.json
index 2e1c4137..0e19ca11 100644
--- a/examples/EcdarUniversity/Queries.json
+++ b/examples/EcdarUniversity/Queries.json
@@ -5,7 +5,7 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
},
{
"query": "specification: Spec",
@@ -13,7 +13,7 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
},
{
"query": "consistency: (Administration || Machine || Researcher)",
@@ -21,7 +21,7 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
},
{
"query": "consistency: (Administration || Machine || Researcher)",
@@ -29,7 +29,7 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
},
{
"query": "refinement: (Administration || Machine || Researcher) \u003c\u003d Spec",
@@ -37,6 +37,6 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
}
]
\ No newline at end of file
diff --git a/examples/FishRetailer/Model/Queries.json b/examples/FishRetailer/Model/Queries.json
index e660ab09..a2131482 100644
--- a/examples/FishRetailer/Model/Queries.json
+++ b/examples/FishRetailer/Model/Queries.json
@@ -5,6 +5,6 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
}
]
\ No newline at end of file
diff --git a/examples/SenderReceiver/Queries.json b/examples/SenderReceiver/Queries.json
index 54261ba5..bf94c503 100644
--- a/examples/SenderReceiver/Queries.json
+++ b/examples/SenderReceiver/Queries.json
@@ -5,7 +5,7 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
},
{
"query": "specification: Sender",
@@ -13,7 +13,7 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
},
{
"query": "implementation: Sender",
@@ -21,6 +21,6 @@
"isPeriodic": false,
"ignoredInputs": {},
"ignoredOutputs": {},
- "backend": "Reveaal"
+ "engine": "Reveaal"
}
]
\ No newline at end of file
diff --git a/presentation/EngineConfiguration.png b/presentation/EngineConfiguration.png
new file mode 100644
index 00000000..38b26daa
Binary files /dev/null and b/presentation/EngineConfiguration.png differ
diff --git a/src/main/java/ecdar/Ecdar.java b/src/main/java/ecdar/Ecdar.java
index 542b788b..8efe1da7 100644
--- a/src/main/java/ecdar/Ecdar.java
+++ b/src/main/java/ecdar/Ecdar.java
@@ -1,16 +1,12 @@
package ecdar;
-import ecdar.abstractions.Component;
-import ecdar.abstractions.Project;
-import ecdar.backend.BackendDriver;
+import ecdar.abstractions.*;
+import ecdar.backend.BackendException;
import ecdar.backend.BackendHelper;
-import ecdar.backend.QueryHandler;
import ecdar.backend.SimulationHandler;
import ecdar.code_analysis.CodeAnalysis;
-import ecdar.controllers.EcdarController;
-import ecdar.presentations.BackgroundThreadPresentation;
-import ecdar.presentations.EcdarPresentation;
-import ecdar.presentations.UndoRedoHistoryPresentation;
+import ecdar.issues.ExitStatusCodes;
+import ecdar.presentations.*;
import ecdar.utility.keyboard.Keybind;
import ecdar.utility.keyboard.KeyboardTracker;
import javafx.application.Application;
@@ -53,14 +49,13 @@ public class Ecdar extends Application {
private static EcdarPresentation presentation;
private static BooleanProperty isUICached = new SimpleBooleanProperty();
public static BooleanProperty shouldRunBackgroundQueries = new SimpleBooleanProperty(true);
- private static final BooleanProperty isSplit = new SimpleBooleanProperty(true); //Set to true to ensure correct behaviour at first toggle.
- private static BackendDriver backendDriver = new BackendDriver();
- private static QueryHandler queryHandler = new QueryHandler(backendDriver);
+ private static final BooleanProperty isSplit = new SimpleBooleanProperty(false);
private static SimulationHandler simulationHandler;
private Stage debugStage;
/**
* Gets the absolute path to the server folder
+ *
* @return
*/
public static String getServerPath() {
@@ -75,6 +70,7 @@ public static String getServerPath() {
/**
* Gets the path to the root directory.
+ *
* @return the path to the root directory
* @throws URISyntaxException if the source code location could not be converted to an URI
*/
@@ -119,6 +115,7 @@ public static void main(final String[] args) {
/**
* Gets the project.
+ *
* @return the project
*/
public static Project getProject() {
@@ -147,6 +144,7 @@ public static BooleanProperty toggleLeftPane() {
* Toggles whether to cache UI.
* Caching reduces CPU usage on some devices.
* It also increases GPU 3D engine usage on some devices.
+ *
* @return the property specifying whether to cache
*/
public static BooleanProperty toggleUICache() {
@@ -157,6 +155,7 @@ public static BooleanProperty toggleUICache() {
/**
* Toggles whether checks are run in the background.
* Running checks in the background increases CPU usage and power consumption.
+ *
* @return the property specifying whether to run checks in the background
*/
public static BooleanProperty toggleRunBackgroundQueries() {
@@ -168,26 +167,15 @@ public static BooleanProperty toggleQueryPane() {
return presentation.toggleRightPane();
}
- /**
- * Toggles whether to canvas is split or single.
- * @return the property specifying whether the canvas is split
- */
- public static BooleanProperty toggleCanvasSplit() {
- isSplit.set(!isSplit.get());
-
+ public static BooleanProperty isSplitProperty() {
return isSplit;
}
/**
- * Returns the backend driver used to execute queries and handle simulation
- * @return BackendDriver
+ * Toggles whether to canvas is split or single.
*/
- public static BackendDriver getBackendDriver() {
- return backendDriver;
- }
-
- public static QueryHandler getQueryExecutor() {
- return queryHandler;
+ public static void toggleCanvasSplit() {
+ isSplit.set(!isSplit.get());
}
public static SimulationHandler getSimulationHandler() { return simulationHandler; }
@@ -213,10 +201,6 @@ public void start(final Stage stage) {
// Print launch message the user, if terminal is being launched
System.out.println("Launching ECDAR...");
- // Load or create new project
- project = new Project();
- simulationHandler = new SimulationHandler(getBackendDriver());
-
// Set the title for the application
stage.setTitle("Ecdar " + VERSION);
@@ -264,13 +248,21 @@ public void start(final Stage stage) {
Ecdar.showToast("The application icon could not be loaded");
}
- // We're now ready! Let the curtains fall!
- stage.show();
+ BackendHelper.addEngineInstanceListener(() -> {
+ // When the engines change, clear the backendDriver
+ // to prevent dangling connections and queries
+ try {
+ BackendHelper.clearEngineConnections();
+ } catch (BackendException e) {
+ showToast("An exception was encountered during shutdown of engine connections");
+ }
+ });
- project.reset();
+ // Whenever the Runtime is requested to exit, exit the Platform first
+ Runtime.getRuntime().addShutdownHook(new Thread(Platform::exit));
- // Set active model
- Platform.runLater(() -> EcdarController.setActiveModelForActiveCanvas(Ecdar.getProject().getComponents().get(0)));
+ // We're now ready! Let the curtains fall!
+ stage.show();
// Register a key-bind for showing debug-information
KeyboardTracker.registerKeybind("DEBUG", new Keybind(new KeyCodeCombination(KeyCode.F12), () -> {
@@ -309,85 +301,23 @@ public void start(final Stage stage) {
}));
stage.setOnCloseRequest(event -> {
- BackendHelper.stopQueries();
+ int statusCode = ExitStatusCodes.SHUTDOWN_SUCCESSFUL.getStatusCode();
try {
- backendDriver.closeAllBackendConnections();
- queryHandler.closeAllBackendConnections();
- simulationHandler.closeAllBackendConnections();
- } catch (IOException e) {
- e.printStackTrace();
+ BackendHelper.clearEngineConnections();
+ } catch (BackendException e) {
+ statusCode = ExitStatusCodes.CLOSE_ENGINE_CONNECTIONS_FAILED.getStatusCode();
}
- Platform.exit();
- System.exit(0);
- });
-
- BackendHelper.addBackendInstanceListener(() -> {
- // When the backend instances change, re-instantiate the backendDriver
- // to prevent dangling connections and queries
try {
- backendDriver.closeAllBackendConnections();
- queryHandler.closeAllBackendConnections();
- simulationHandler.closeAllBackendConnections();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
-
- backendDriver = new BackendDriver();
- });
- }
-
- /**
- * Initializes and resets the project.
- * This can be used as a test setup.
- */
- public static void setUpForTest() {
- project = new Project();
- project.reset();
-
- // This implicitly starts the fx-application thread
- // It prevents java.lang.RuntimeException: Internal graphics not initialized yet
- // https://stackoverflow.com/questions/27839441/internal-graphics-not-initialized-yet-javafx
- // new JFXPanel();
- }
-
- public static void initializeProjectFolder() throws IOException {
- // Make sure that the project directory exists
- final File directory = new File(projectDirectory.get());
- FileUtils.forceMkdir(directory);
-
- CodeAnalysis.getErrors().addListener(new ListChangeListener() {
- @Override
- public void onChanged(Change extends CodeAnalysis.Message> c) {
- CodeAnalysis.getErrors().forEach(message -> {
- System.out.println(message.getMessage());
- });
+ System.exit(statusCode);
+ } catch (SecurityException e) {
+ // Forcefully shutdown the Java Runtime
+ Runtime.getRuntime().halt(ExitStatusCodes.GRACEFUL_SHUTDOWN_FAILED.getStatusCode());
}
});
- CodeAnalysis.clearErrorsAndWarnings();
- CodeAnalysis.disable();
- getProject().clean();
-
- // Deserialize the project
- Ecdar.getProject().deserialize(directory);
- CodeAnalysis.enable();
- // Generate all component presentations by making them the active component in the view one by one
- Component initialShownComponent = null;
- for (final Component component : Ecdar.getProject().getComponents()) {
- // The first component should be shown
- if (initialShownComponent == null) {
- initialShownComponent = component;
- }
- EcdarController.setActiveModelForActiveCanvas(component);
- }
-
- // If we found a component set that as active
- if (initialShownComponent != null) {
- EcdarController.setActiveModelForActiveCanvas(initialShownComponent);
- }
- serializationDone = true;
+ project = presentation.getController().getEditorPresentation().getController().projectPane.getController().project;
}
private void loadFonts() {
@@ -422,6 +352,44 @@ private void loadFonts() {
Font.loadFont(getClass().getResourceAsStream("fonts/roboto_mono/RobotoMono-ThinItalic.ttf"), 14);
}
+ /**
+ * Initializes and resets the project.
+ * This can be used as a test setup.
+ */
+ public static void setUpForTest() {
+ project = new Project();
+
+ // This implicitly starts the fx-application thread
+ // It prevents java.lang.RuntimeException: Internal graphics not initialized yet
+ // https://stackoverflow.com/questions/27839441/internal-graphics-not-initialized-yet-javafx
+ // new JFXPanel();
+ }
+
+ public static void initializeProjectFolder() throws IOException {
+ // Make sure that the project directory exists
+ final File directory = new File(projectDirectory.get());
+ FileUtils.forceMkdir(directory);
+
+ CodeAnalysis.getErrors().addListener((ListChangeListener) c -> CodeAnalysis.getErrors().forEach(message -> {
+ System.out.println(message.getMessage());
+ }));
+
+ CodeAnalysis.clearErrorsAndWarnings();
+ CodeAnalysis.disable();
+ getProject().clean();
+
+ // Deserialize the project
+ getProject().deserialize(directory);
+ CodeAnalysis.enable();
+
+ // If we found a component set that as active
+ serializationDone = true;
+ }
+
+ public static ComponentPresentation getComponentPresentationOfComponent(Component component) {
+ return getPresentation().getController().getEditorPresentation().getController().projectPane.getController().getComponentPresentations().stream().filter(componentPresentation -> componentPresentation.getController().getComponent().equals(component)).findFirst().orElse(null);
+ }
+
private static String getVersion() {
Properties defaultProps = new Properties();
try {
diff --git a/src/main/java/ecdar/abstractions/Component.java b/src/main/java/ecdar/abstractions/Component.java
index 1d0d38c0..b83808a2 100644
--- a/src/main/java/ecdar/abstractions/Component.java
+++ b/src/main/java/ecdar/abstractions/Component.java
@@ -1,36 +1,28 @@
package ecdar.abstractions;
-import com.bpodgursky.jbool_expressions.*;
-import com.bpodgursky.jbool_expressions.rules.RuleSet;
-import ecdar.Ecdar;
-import ecdar.utility.ExpressionHelper;
import ecdar.utility.UndoRedoStack;
-import ecdar.utility.colors.Color;
import ecdar.utility.colors.EnabledColor;
import ecdar.utility.helpers.Boxed;
+import ecdar.utility.helpers.MouseCircular;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
-import ecdar.utility.helpers.MouseCircular;
-import ecdar.utility.keyboard.NudgeDirection;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
-import javafx.collections.transformation.FilteredList;
-import javafx.util.Pair;
-import org.apache.commons.lang3.tuple.Triple;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import static ecdar.abstractions.Project.LOCATION;
+
/**
* A component that models an I/O automata.
*/
-public class Component extends HighLevelModelObject implements Boxed {
- private static final String COMPONENT = "Component";
+public class Component extends HighLevelModel implements Boxed {
private static final String LOCATIONS = "locations";
private static final String EDGES = "edges";
private static final String INCLUDE_IN_PERIODIC_CHECK = "includeInPeriodicCheck";
@@ -53,7 +45,6 @@ public class Component extends HighLevelModelObject implements Boxed {
// Styling properties
private final Box box = new Box();
private final BooleanProperty declarationOpen = new SimpleBooleanProperty(false);
- private final BooleanProperty firsTimeShown = new SimpleBooleanProperty(false);
public Location previousLocationForDraggedEdge;
public boolean getIsFailing(){return isFailing.get();}
@@ -71,272 +62,32 @@ public Component() {
/**
* Creates a component with a specific name and a boolean value that chooses whether the colour for this component is chosen at random
- * @param doRandomColor boolean that is true if the component should choose a colour at random and false if not
+ * @param color boolean that is true if the component should choose a colour at random and false if not
*/
- public Component(final boolean doRandomColor) {
- setComponentName();
-
- if(doRandomColor) {
- setRandomColor();
- }
+ public Component(final EnabledColor color, final String name) {
+ setName(name);
+ setColor(color);
// Make initial location
final Location initialLocation = new Location();
- initialLocation.initialize();
+ initialLocation.initialize(LOCATION + "1");
initialLocation.setType(Location.Type.INITIAL);
- initialLocation.setColorIntensity(getColorIntensity());
- initialLocation.setColor(getColor());
// Place in center
initialLocation.setX(box.getX() + box.getWidth() / 2);
initialLocation.setY(box.getY() + box.getHeight() / 2);
+ initialLocation.setColor(color);
- locations.add(initialLocation);
+ addLocation(initialLocation);
initializeIOListeners();
}
public Component(final JsonObject json) {
- setFirsTimeShown(true);
-
deserialize(json);
initializeIOListeners();
updateIOList();
}
- /**
- * Creates a clone of another component.
- * Copies objects used for verification (e.g. locations, edges and the declarations).
- * Does not copy UI elements (sizes and positions).
- * It locations are cloned from the original component. Their ids are the same.
- * Does not initialize io listeners, but copies the input and output strings.
- * Reachability analysis binding is not initialized.
- * @return the clone
- */
- public Component cloneForVerification() {
- final Component clone = new Component();
- clone.addVerificationObjects(this);
- clone.setIncludeInPeriodicCheck(false);
- clone.inputStrings.addAll(getInputStrings());
- clone.outputStrings.addAll(getOutputStrings());
- clone.setName(getName());
-
- return clone;
- }
-
- /**
- * Adds objects used for verifications to this component.
- * @param original the component to add from
- */
- private void addVerificationObjects(final Component original) {
- for (final Location originalLoc : original.getLocations()) {
- addLocation(originalLoc.cloneForVerification());
- }
-
- getListOfEdgesFromDisplayableEdges(original.getDisplayableEdges()).forEach(edge -> addEdge((edge).cloneForVerification(this)));
- setDeclarationsText(original.getDeclarationsText());
- }
-
- /**
- * Applies angelic completion on this component.
- */
- public void applyAngelicCompletion() {
- // Cache input signature, since it could be updated when added edges
- final List inputStrings = new ArrayList<>(getInputStrings());
-
- getLocations().forEach(location -> inputStrings.forEach(input -> {
- // Get outgoing input edges that has the chosen input
- final List matchingEdges = getListOfEdgesFromDisplayableEdges(getOutgoingEdges(location)).stream().filter(
- edge -> edge.getStatus().equals(EdgeStatus.INPUT) &&
- edge.getSync().equals(input)).collect(Collectors.toList()
- );
-
- // If no such edges, add a self loop without a guard
- if (matchingEdges.isEmpty()) {
- final Edge edge = new Edge(location, EdgeStatus.INPUT);
- edge.setTargetLocation(location);
- edge.addSyncNail(input);
- addEdge(edge);
- return;
- }
-
- // If an edge has no guard and its target has no invariants, ignore.
- // Component is already input-enabled with respect to this location and input.
- if (matchingEdges.stream().anyMatch(edge -> edge.getGuard().isEmpty() &&
- edge.getTargetLocation().getInvariant().isEmpty())) return;
-
- // Extract expression for which edges to create.
- // The expression is in DNF
- // We create self loops for each child expression in the disjunction.
- createAngelicSelfLoops(location, input, getNegatedEdgeExpression(matchingEdges));
- }));
- }
-
- /**
- * Extracts an expression that represents the negation of a list of edges.
- * Multiple edges are resolved as disjunctions.
- * There can be conjunction in guards.
- * We translate each edge to the conjunction of its guard and the invariant of its target location
- * (since it should also be satisfied).
- * The result is converted to disjunctive normal form.
- * @param edges the edges to extract from
- * @return the expression that represents the negation
- */
- private Expression getNegatedEdgeExpression(final List edges) {
- final List clocks = getClocks();
- return ExpressionHelper.simplifyNegatedSimpleExpressions(
- RuleSet.toDNF(RuleSet.simplify(Not.of(Or.of(edges.stream()
- .map(edge -> {
- final List clocksToReset = ExpressionHelper.getUpdateSides(edge.getUpdate())
- .keySet().stream().filter(clocks::contains).collect(Collectors.toList());
- return And.of(
- ExpressionHelper.parseGuard(edge.getGuard()),
- ExpressionHelper.parseInvariantButIgnore(edge.getTargetLocation().getInvariant(), clocksToReset)
- );
- }
- ).collect(Collectors.toList()))
- ))));
- }
-
- /**
- * Creates self loops on a location to finish missing inputs with an angelic completion.
- * The guard expression should be in DNF without negations.
- * @param location the location to create self loops on
- * @param input the input action to use in the synchronization properties
- * @param guardExpression the expression that represents the guards of the self loops
- */
- private void createAngelicSelfLoops(final Location location, final String input, final Expression guardExpression) {
- final Edge edge;
-
- switch (guardExpression.getExprType()) {
- case Literal.EXPR_TYPE:
- // If false, do not create any loops
- if (!((Literal) guardExpression).getValue()) break;
-
- // It should never be true, since that should be handled before calling this method
- throw new RuntimeException("Type of expression " + guardExpression + " not accepted");
- case Variable.EXPR_TYPE:
- edge = new Edge(location, EdgeStatus.INPUT);
- edge.setTargetLocation(location);
- edge.addSyncNail(input);
- edge.addGuardNail(((Variable) guardExpression).getValue());
- addEdge(edge);
- break;
- case And.EXPR_TYPE:
- edge = new Edge(location, EdgeStatus.INPUT);
- edge.setTargetLocation(location);
- edge.addSyncNail(input);
- edge.addGuardNail(String.join("&&",
- ((And) guardExpression).getChildren().stream()
- .map(child -> {
- if (!child.getExprType().equals(Variable.EXPR_TYPE))
- throw new RuntimeException("Child " + child + " of type " +
- child.getExprType() + " in and expression " +
- guardExpression + " should be a variable");
-
- return ((Variable) child).getValue();
- })
- .collect(Collectors.toList())
- ));
- addEdge(edge);
- break;
- case Or.EXPR_TYPE:
- guardExpression.getChildren().forEach(child -> createAngelicSelfLoops(location, input, child));
- break;
- default:
- throw new RuntimeException("Type of expression " + guardExpression + " not accepted");
- }
- }
-
- /**
- * Applies demonic completion on this component.
- */
- public void applyDemonicCompletion() {
- // Make a universal location
- final Location uniLocation = new Location(this, Location.Type.UNIVERSAL, 0, 0);
- addLocation(uniLocation);
-
- final Edge inputEdge = uniLocation.addLeftEdge("*", EdgeStatus.INPUT);
- inputEdge.setIsLocked(true);
- addEdge(inputEdge);
-
- final Edge outputEdge = uniLocation.addRightEdge("*", EdgeStatus.OUTPUT);
- outputEdge.setIsLocked(true);
- addEdge(outputEdge);
-
- // Cache input signature, since it could be updated when added edges
- final List inputStrings = new ArrayList<>(getInputStrings());
-
- getLocations().forEach(location -> inputStrings.forEach(input -> {
- // Get outgoing input edges that has the chosen input
- final List matchingEdges = getListOfEdgesFromDisplayableEdges(getOutgoingEdges(location)).stream().filter(
- edge -> edge.getStatus().equals(EdgeStatus.INPUT) &&
- edge.getSync().equals(input)).collect(Collectors.toList()
- );
-
- // If no such edges, add an edge to Universal
- if (matchingEdges.isEmpty()) {
- final Edge edge = new Edge(location, EdgeStatus.INPUT);
- edge.setTargetLocation(uniLocation);
- edge.addSyncNail(input);
- addEdge(edge);
- return;
- }
- // If an edge has no guard and its target has no invariants, ignore.
- // Component is already input-enabled with respect to this location and input.
- if (matchingEdges.stream().anyMatch(edge -> edge.getGuard().isEmpty() &&
- edge.getTargetLocation().getInvariant().isEmpty())) return;
-
- // Extract expression for which edges to create.
- // The expression is in DNF
- // We create edges to Universal for each child expression in the disjunction.
- createDemonicEdges(location, uniLocation, input, getNegatedEdgeExpression(matchingEdges));
- }));
- }
-
- /**
- * Creates edges to a specified Universal location from a location
- * in order to finish missing inputs with a demonic completion.
- * @param location the location to create self loops on
- * @param universal the Universal location to create edge to
- * @param input the input action to use in the synchronization properties
- * @param guardExpression the expression that represents the guards of the edges to create
- */
- private void createDemonicEdges(final Location location, final Location universal, final String input, final Expression guardExpression) {
- final Edge edge;
-
- switch (guardExpression.getExprType()) {
- case Literal.EXPR_TYPE:
- // If false, do not create any edges
- if (!((Literal) guardExpression).getValue()) break;
-
- // It should never be true, since that should be handled before calling this method
- throw new RuntimeException("Type of expression " + guardExpression + " not accepted");
- case Variable.EXPR_TYPE:
- edge = new Edge(location, EdgeStatus.INPUT);
- edge.setTargetLocation(universal);
- edge.addSyncNail(input);
- edge.addGuardNail(((Variable) guardExpression).getValue());
- addEdge(edge);
- break;
- case And.EXPR_TYPE:
- edge = new Edge(location, EdgeStatus.INPUT);
- edge.setTargetLocation(universal);
- edge.addSyncNail(input);
- edge.addGuardNail(String.join("&&",
- ((And) guardExpression).getChildren().stream()
- .map(child -> ((Variable) child).getValue())
- .collect(Collectors.toList())
- ));
- addEdge(edge);
- break;
- case Or.EXPR_TYPE:
- ((Or) guardExpression).getChildren().forEach(child -> createDemonicEdges(location, universal, input, child));
- break;
- default:
- throw new RuntimeException("Type of expression " + guardExpression + " not accepted");
- }
- }
-
/**
* Initialises IO listeners, adding change listener to the list of edges
* Also adds listeners to all current edges in edges.
@@ -383,7 +134,7 @@ private void initializeIOListeners() {
});
// Add listener to edges initially
- edges.forEach(edge -> getEdgeOrSubEdges(edge).forEach(subEdge -> addSyncListener(listener, subEdge)));
+ getEdges().forEach(edge -> getEdgeOrSubEdges(edge).forEach(subEdge -> addSyncListener(listener, subEdge)));
}
/**
@@ -396,60 +147,6 @@ private static void addSyncListener(final ChangeListener listener, final
edge.ioStatus.addListener(listener);
}
- /**
- * Method used for updating the inputstrings and outputstrings list
- * Sorts the list alphabetically, ignoring case
- */
- public void updateIOList() {
- final List localInputStrings = new ArrayList<>();
- final List localOutputStrings = new ArrayList<>();
-
- List edgeList = getListOfEdgesFromDisplayableEdges(edges);
-
- for (final Edge edge : edgeList) {
- // Extract channel id based on UPPAAL id definition
- final String channel = edge.getSync().replaceAll("^([a-zA-Z_][a-zA-Z0-9_]*).*$", "$1");
-
- if(edge.getStatus() == EdgeStatus.INPUT){
- if(!edge.getSync().equals("*") && !localInputStrings.contains(channel)){
- localInputStrings.add(channel);
- }
- } else if (edge.getStatus() == EdgeStatus.OUTPUT) {
- if(!edge.getSync().equals("*") && !localOutputStrings.contains(channel)){
- localOutputStrings.add(channel);
- }
- }
- }
-
- // Sort the String alphabetically, ignoring case (e.g. all strings starting with "C" are placed together)
- localInputStrings.sort((item1, item2) -> item1.compareToIgnoreCase(item2));
- localOutputStrings.sort((item1, item2) -> item1.compareToIgnoreCase(item2));
-
- inputStrings.setAll(localInputStrings);
- outputStrings.setAll(localOutputStrings);
- }
-
- private List getListOfEdgesFromDisplayableEdges(List displayableEdges) {
- List result = new ArrayList<>();
- for (final DisplayableEdge originalEdge : displayableEdges) {
- result.addAll(getEdgeOrSubEdges(originalEdge));
- }
-
- return result;
- }
-
- private List getEdgeOrSubEdges(DisplayableEdge edge) {
- List result = new ArrayList<>();
-
- if(edge instanceof Edge) {
- result.add((Edge) edge);
- } else {
- result.addAll(((GroupedEdge) edge).getEdges());
- }
-
- return result;
- }
-
/**
* Get all locations in this, but the initial location (if one exists).
* O(n), n is # of locations in component.
@@ -492,6 +189,15 @@ public boolean removeLocation(final Location location) {
return locations.remove(location);
}
+ public String getUniqueLocationId() {
+ final var currentIds = getLocations().stream().map(Location::getId).collect(Collectors.toSet());
+ for (int counter = 1; ; counter++) {
+ if (!currentIds.contains(LOCATION + counter)) {
+ return LOCATION + counter;
+ }
+ }
+ }
+
/**
* Add a failing Edge to the list of failing edges
* and set the edge's failing property to true.
@@ -605,6 +311,27 @@ public synchronized List getOutgoingEdges(final Location locati
return getDisplayableEdges().filtered(edge -> location == edge.getSourceLocation());
}
+ public List getListOfEdgesFromDisplayableEdges(List displayableEdges) {
+ List result = new ArrayList<>();
+ for (final DisplayableEdge originalEdge : displayableEdges) {
+ result.addAll(getEdgeOrSubEdges(originalEdge));
+ }
+
+ return result;
+ }
+
+ private List getEdgeOrSubEdges(DisplayableEdge edge) {
+ List result = new ArrayList<>();
+
+ if(edge instanceof Edge) {
+ result.add((Edge) edge);
+ } else {
+ result.addAll(((GroupedEdge) edge).getEdges());
+ }
+
+ return result;
+ }
+
public boolean isDeclarationOpen() {
return declarationOpen.get();
}
@@ -655,18 +382,6 @@ public DisplayableEdge getUnfinishedEdge() {
return null;
}
- public boolean isFirsTimeShown() {
- return firsTimeShown.get();
- }
-
- public BooleanProperty firsTimeShownProperty() {
- return firsTimeShown;
- }
-
- public void setFirsTimeShown(final boolean firsTimeShown) {
- this.firsTimeShown.set(firsTimeShown);
- }
-
public boolean isIncludeInPeriodicCheck() {
return includeInPeriodicCheck.get();
}
@@ -679,157 +394,6 @@ public void setIncludeInPeriodicCheck(final boolean includeInPeriodicCheck) {
this.includeInPeriodicCheck.set(includeInPeriodicCheck);
}
- /**
- * Checks if there is currently an edge without a source location.
- */
- public boolean isAnyEdgeWithoutSource() {
- DisplayableEdge edgeWithoutSource = null;
-
- for (DisplayableEdge edge : getDisplayableEdges()) {
- if (edge.sourceCircularProperty().get() instanceof MouseCircular) {
- edgeWithoutSource = edge;
- break;
- }
- }
-
- return edgeWithoutSource != null;
- }
-
- @Override
- public JsonObject serialize() {
- final JsonObject result = super.serialize();
- result.addProperty(DECLARATIONS, getDeclarationsText());
-
- final JsonArray locations = new JsonArray();
- getLocations().forEach(location -> locations.add(location.serialize()));
- result.add(LOCATIONS, locations);
-
- final JsonArray edges = new JsonArray();
- getListOfEdgesFromDisplayableEdges(this.edges).forEach(edge -> edges.add(edge.serialize()));
-
- result.add(EDGES, edges);
- result.addProperty(DESCRIPTION, getDescription());
- box.addProperties(result);
- result.addProperty(COLOR, EnabledColor.getIdentifier(getColor()));
- result.addProperty(INCLUDE_IN_PERIODIC_CHECK, isIncludeInPeriodicCheck());
-
- return result;
- }
-
- @Override
- public void deserialize(final JsonObject json) {
- super.deserialize(json);
-
- setDeclarationsText(json.getAsJsonPrimitive(DECLARATIONS).getAsString());
-
- json.getAsJsonArray(LOCATIONS).forEach(jsonElement -> {
- final Location newLocation = new Location((JsonObject) jsonElement);
- locations.add(newLocation);
- });
-
- json.getAsJsonArray(EDGES).forEach(jsonElement -> {
- JsonObject edgeObject = (JsonObject) jsonElement;
- final Edge newEdge = new Edge((JsonObject) jsonElement, this);
-
- String edgeGroup = "";
- if (edgeObject.has("group")) {
- edgeGroup = edgeObject.getAsJsonPrimitive("group").getAsString();
- }
-
- if (!edgeGroup.isEmpty()) {
- GroupedEdge groupedEdge = null;
- for (DisplayableEdge edge : edges) {
- if (edge instanceof GroupedEdge && edge.getId().equals(edgeGroup)) {
- groupedEdge = ((GroupedEdge) edge);
- break;
- }
- }
-
- if (groupedEdge == null) {
- GroupedEdge newGroupedEdge = new GroupedEdge(newEdge);
- newGroupedEdge.setId(edgeGroup);
-
- 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("");
- edges.add(newEdge);
- }
- }
- } else {
- edges.add(newEdge);
- }
- });
-
- setDescription(json.getAsJsonPrimitive(DESCRIPTION).getAsString());
- box.setProperties(json);
-
- if (box.getWidth() == 0 && box.getHeight() == 0) {
- box.setWidth(locations.stream().max(Comparator.comparingDouble(Location::getX)).get().getX() + Ecdar.CANVAS_PADDING * 10);
- box.setHeight(locations.stream().max(Comparator.comparingDouble(Location::getY)).get().getY() + Ecdar.CANVAS_PADDING * 10);
- }
-
- final EnabledColor enabledColor = (json.has(COLOR) ? EnabledColor.fromIdentifier(json.getAsJsonPrimitive(COLOR).getAsString()) : null);
- if (enabledColor != null) {
- setColorIntensity(enabledColor.intensity);
- setColor(enabledColor.color);
- } else {
- setRandomColor();
- for(Location loc : locations) {
- loc.setColor(this.getColor());
- loc.setColorIntensity(this.getColorIntensity());
- }
- }
-
- if(json.has(INCLUDE_IN_PERIODIC_CHECK)) {
- setIncludeInPeriodicCheck(json.getAsJsonPrimitive(INCLUDE_IN_PERIODIC_CHECK).getAsBoolean());
- } else {
- setIncludeInPeriodicCheck(false);
- }
- }
-
- /**
- * Dyes the component and its locations.
- * @param color the color to dye with
- * @param intensity the intensity of the color
- */
- public void dye(final Color color, final Color.Intensity intensity) {
- final Color previousColor = colorProperty().get();
- final Color.Intensity previousColorIntensity = colorIntensityProperty().get();
-
- final Map> previousLocationColors = new HashMap<>();
-
- for (final Location location : getLocations()) {
- if (!location.getColor().equals(previousColor)) continue;
- previousLocationColors.put(location, new Pair<>(location.getColor(), location.getColorIntensity()));
- }
-
- UndoRedoStack.pushAndPerform(() -> { // Perform
- // Color the component
- setColorIntensity(intensity);
- setColor(color);
-
- // Color all of the locations
- previousLocationColors.keySet().forEach(location -> {
- location.setColorIntensity(intensity);
- location.setColor(color);
- });
- }, () -> { // Undo
- // Color the component
- setColorIntensity(previousColorIntensity);
- setColor(previousColor);
-
- // Color the locations accordingly to the previous color for them
- previousLocationColors.keySet().forEach(location -> {
- location.setColorIntensity(previousLocationColors.get(location).getValue());
- location.setColor(previousLocationColors.get(location).getKey());
- });
- }, String.format("Changed the color of %s to %s", this, color.name()), "color-lens");
- }
-
public String getDescription() {
return description.get();
}
@@ -850,36 +414,6 @@ public ObservableList getOutputStrings() {
return outputStrings;
}
- /**
- * Generate and sets a unique id for this system
- */
- private void setComponentName() {
- for(int counter = 1; ; counter++) {
- if(!Ecdar.getProject().getComponentNames().contains(COMPONENT + counter)){
- setName((COMPONENT + counter));
- return;
- }
- }
- }
-
- /**
- * Generates an id to be used by universal and inconsistent locations in this component,
- * if one has already been generated, return that instead
- * @return generated universal/inconsistent id
- */
- String generateUniIncId(){
- final String id = getUniIncId();
- if(id != null){
- return id;
- } else {
- for(int counter = 0; ;counter++){
- if(!Ecdar.getProject().getUniIncIds().contains(String.valueOf(counter))){
- return String.valueOf(counter);
- }
- }
- }
- }
-
/**
* Gets the id used by universal and inconsistent locations located in this component,
* if neither universal nor inconsistent locations exist in this component it returns null.
@@ -894,32 +428,6 @@ public String getUniIncId() {
return null;
}
- /**
- * Gets the id of all locations in this component
- * @return ids of all locations in this component
- */
- HashSet getLocationIds() {
- final HashSet ids = new HashSet<>();
- for (final Location location : getLocations()){
- if(location.getType() != Location.Type.UNIVERSAL || location.getType() != Location.Type.INCONSISTENT){
- ids.add(location.getId().substring(Location.ID_LETTER_LENGTH));
- }
- }
- return ids;
- }
-
- /**
- * Gets the id of all edges in this component
- * @return ids of all edges in this component
- */
- HashSet getEdgeIds() {
- final HashSet ids = new HashSet<>();
- for (final Edge edge : getEdges()){
- ids.add(edge.getId().substring(Edge.ID_LETTER_LENGTH));
- }
- return ids;
- }
-
public String getDeclarationsText() {
return declarationsText.get();
}
@@ -932,11 +440,6 @@ public StringProperty declarationsTextProperty() {
return declarationsText;
}
- @Override
- public Box getBox() {
- return box;
- }
-
/**
* Gets the clocks defined in the declarations text.
* @return the clocks
@@ -969,79 +472,188 @@ public List getLocalVariables() {
return locals;
}
+ public List getInputEdges() {
+ return getDisplayableEdges().filtered(edge -> edge.getStatus().equals(EdgeStatus.INPUT));
+ }
+
+ public List getOutputEdges() {
+ return getDisplayableEdges().filtered(edge -> edge.getStatus().equals(EdgeStatus.OUTPUT));
+ }
+
/**
- * Gets the local variables defined in the declarations text.
- * Also gets the lower and upper bounds for these variables.
- * @return Triples containing (left) name of the variable, (middle) lower bound, (right) upper bound
+ * Dyes the component and its locations.
+ * @param color the color to dye with
*/
- public List> getLocalVariablesWithBounds() {
- final List> typedefs = Ecdar.getProject().getGlobalDeclarations().getTypedefs();
+ public void dye(final EnabledColor color) {
+ final EnabledColor previousColor = colorProperty().get();
- final List> locals = new ArrayList<>();
+ final Map previousLocationColors = new HashMap<>();
- Arrays.stream(getDeclarationsText().split(";")).forEach(statement -> {
- final Matcher matcher = Pattern.compile("^\\s*(\\w+)\\s+(\\w+)(\\W|$)").matcher(statement);
- if (!matcher.find()) return;
+ for (final Location location : getLocations()) {
+ if (!location.getColor().equals(previousColor)) continue;
+ previousLocationColors.put(location, location.getColor());
+ }
- final Optional> typedef = typedefs.stream()
- .filter(def -> def.getLeft().equals(matcher.group(1))).findAny();
- if (!typedef.isPresent()) return;
+ UndoRedoStack.pushAndPerform(() -> { // Perform
+ // Color the component
+ setColor(color);
- locals.add(Triple.of(matcher.group(2), typedef.get().getMiddle(), typedef.get().getRight()));
- });
+ // Color all of the locations
+ previousLocationColors.keySet().forEach(location -> {
+ location.setColor(color);
+ });
+ }, () -> { // Undo
+ // Color the component
+ setColor(previousColor);
- return locals;
+ // Color the locations accordingly to the previous color for them
+ previousLocationColors.keySet().forEach(location -> {
+ location.setColor(previousLocationColors.get(location));
+ });
+ }, String.format("Changed the color of %s to %s", this, color.color.name()), "color-lens");
}
/**
- * Gets the first occurring universal location in this component.
- * @return the first universal location, or null if none exists
+ * Checks if there is currently an edge without a source location.
*/
- public Location getUniversalLocation() {
- final FilteredList uniLocs = getLocations().filtered(l -> l.getType().equals(Location.Type.UNIVERSAL));
+ public boolean isAnyEdgeWithoutSource() {
+ DisplayableEdge edgeWithoutSource = null;
- if (uniLocs.isEmpty()) return null;
+ for (DisplayableEdge edge : getDisplayableEdges()) {
+ if (edge.sourceCircularProperty().get() instanceof MouseCircular) {
+ edgeWithoutSource = edge;
+ break;
+ }
+ }
- return uniLocs.get(0);
+ return edgeWithoutSource != null;
}
/**
- * Moves all nodes left.
+ * Method used for updating the inputstrings and outputstrings list
+ * Sorts the list alphabetically, ignoring case
*/
- public void moveAllNodesLeft() {
- getLocations().forEach(loc -> loc.setX(loc.getX() + NudgeDirection.LEFT.getXOffset()));
- getDisplayableEdges().forEach(edge -> edge.getNails().forEach(nail -> nail.setX(nail.getX() + NudgeDirection.LEFT.getXOffset())));
- }
+ public void updateIOList() {
+ final List localInputStrings = new ArrayList<>();
+ final List localOutputStrings = new ArrayList<>();
- /**
- * Moves all nodes right.
- */
- public void moveAllNodesRight() {
- getLocations().forEach(loc -> loc.setX(loc.getX() + NudgeDirection.RIGHT.getXOffset()));
- getDisplayableEdges().forEach(edge -> edge.getNails().forEach(nail -> nail.setX(nail.getX() + NudgeDirection.RIGHT.getXOffset())));
- }
+ List edgeList = getListOfEdgesFromDisplayableEdges(edges);
- /**
- * Moves all nodes down.
- */
- public void moveAllNodesDown() {
- getLocations().forEach(loc -> loc.setY(loc.getY() + NudgeDirection.DOWN.getYOffset()));
- getDisplayableEdges().forEach(edge -> edge.getNails().forEach(nail -> nail.setY(nail.getY() + NudgeDirection.DOWN.getYOffset())));
+ for (final Edge edge : edgeList) {
+ // Extract channel id based on UPPAAL id definition
+ final String channel = edge.getSync().replaceAll("^([a-zA-Z_][a-zA-Z0-9_]*).*$", "$1");
+
+ if(edge.getStatus() == EdgeStatus.INPUT){
+ if(!edge.getSync().equals("*") && !localInputStrings.contains(channel)){
+ localInputStrings.add(channel);
+ }
+ } else if (edge.getStatus() == EdgeStatus.OUTPUT) {
+ if(!edge.getSync().equals("*") && !localOutputStrings.contains(channel)){
+ localOutputStrings.add(channel);
+ }
+ }
+ }
+
+ // Sort the String alphabetically, ignoring case (e.g. all strings starting with "C" are placed together)
+ localInputStrings.sort((item1, item2) -> item1.compareToIgnoreCase(item2));
+ localOutputStrings.sort((item1, item2) -> item1.compareToIgnoreCase(item2));
+
+ inputStrings.setAll(localInputStrings);
+ outputStrings.setAll(localOutputStrings);
}
- /**
- * Moves all nodes up.
- */
- public void moveAllNodesUp() {
- getLocations().forEach(loc -> loc.setY(loc.getY() + NudgeDirection.UP.getYOffset()));
- getDisplayableEdges().forEach(edge -> edge.getNails().forEach(nail -> nail.setY(nail.getY() + NudgeDirection.UP.getYOffset())));
+ @Override
+ public Box getBox() {
+ return box;
}
- public List getInputEdges() {
- return getDisplayableEdges().filtered(edge -> edge.getStatus().equals(EdgeStatus.INPUT));
+ @Override
+ public JsonObject serialize() {
+ final JsonObject result = super.serialize();
+ result.addProperty(DECLARATIONS, getDeclarationsText());
+
+ final JsonArray locations = new JsonArray();
+ getLocations().forEach(location -> locations.add(location.serialize()));
+ result.add(LOCATIONS, locations);
+
+ final JsonArray edges = new JsonArray();
+ getListOfEdgesFromDisplayableEdges(this.edges).forEach(edge -> edges.add(edge.serialize()));
+
+ result.add(EDGES, edges);
+ result.addProperty(DESCRIPTION, getDescription());
+ box.addProperties(result);
+ result.addProperty(COLOR, EnabledColor.getIdentifier(getColor().color));
+ result.addProperty(INCLUDE_IN_PERIODIC_CHECK, isIncludeInPeriodicCheck());
+
+ return result;
}
- public List getOutputEdges() {
- return getDisplayableEdges().filtered(edge -> edge.getStatus().equals(EdgeStatus.OUTPUT));
+ @Override
+ public void deserialize(final JsonObject json) {
+ super.deserialize(json);
+
+ setDeclarationsText(json.getAsJsonPrimitive(DECLARATIONS).getAsString());
+
+ json.getAsJsonArray(LOCATIONS).forEach(jsonElement -> {
+ final Location newLocation = new Location((JsonObject) jsonElement);
+ locations.add(newLocation);
+ });
+
+ json.getAsJsonArray(EDGES).forEach(jsonElement -> {
+ JsonObject edgeObject = (JsonObject) jsonElement;
+ final Edge newEdge = new Edge((JsonObject) jsonElement, this);
+
+ String edgeGroup = "";
+ if (edgeObject.has("group")) {
+ edgeGroup = edgeObject.getAsJsonPrimitive("group").getAsString();
+ }
+
+ if (!edgeGroup.isEmpty()) {
+ GroupedEdge groupedEdge = null;
+ for (DisplayableEdge edge : edges) {
+ if (edge instanceof GroupedEdge && edge.getId().equals(edgeGroup)) {
+ groupedEdge = ((GroupedEdge) edge);
+ break;
+ }
+ }
+
+ if (groupedEdge == null) {
+ GroupedEdge newGroupedEdge = new GroupedEdge(newEdge);
+ newGroupedEdge.setId(edgeGroup);
+
+ 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("");
+ edges.add(newEdge);
+ }
+ }
+ } else {
+ edges.add(newEdge);
+ }
+ });
+
+ setDescription(json.getAsJsonPrimitive(DESCRIPTION).getAsString());
+ box.setProperties(json);
+
+ if (box.getWidth() == 0 && box.getHeight() == 0) {
+ box.setWidth(locations.stream().max(Comparator.comparingDouble(Location::getX)).get().getX() + 10 * 10);
+ box.setHeight(locations.stream().max(Comparator.comparingDouble(Location::getY)).get().getY() + 10 * 10);
+ }
+
+ final EnabledColor enabledColor = (json.has(COLOR) ? EnabledColor.fromIdentifier(json.getAsJsonPrimitive(COLOR).getAsString()) : EnabledColor.getDefault());
+ setColor(enabledColor);
+ for(Location loc : locations) {
+ loc.setColor(enabledColor);
+ }
+
+ if(json.has(INCLUDE_IN_PERIODIC_CHECK)) {
+ setIncludeInPeriodicCheck(json.getAsJsonPrimitive(INCLUDE_IN_PERIODIC_CHECK).getAsBoolean());
+ } else {
+ setIncludeInPeriodicCheck(false);
+ }
}
}
diff --git a/src/main/java/ecdar/abstractions/Declarations.java b/src/main/java/ecdar/abstractions/Declarations.java
index 2014d729..3f42d953 100644
--- a/src/main/java/ecdar/abstractions/Declarations.java
+++ b/src/main/java/ecdar/abstractions/Declarations.java
@@ -2,6 +2,7 @@
import ecdar.utility.colors.Color;
import com.google.gson.JsonObject;
+import ecdar.utility.colors.EnabledColor;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.apache.commons.lang3.tuple.Triple;
@@ -15,7 +16,7 @@
* Overall declarations of a model.
* This could be global declarations or system declarations.
*/
-public class Declarations extends HighLevelModelObject {
+public class Declarations extends HighLevelModel {
private final StringProperty declarationsText = new SimpleStringProperty("");
/**
@@ -24,13 +25,13 @@ public class Declarations extends HighLevelModelObject {
*/
public Declarations(final String name) {
setName(name);
- setColor(Color.AMBER);
+ setColor(EnabledColor.getDefault());
}
public Declarations(final JsonObject json) {
deserialize(json);
- setColor(Color.AMBER);
+ setColor(EnabledColor.getDefault());
}
public String getDeclarationsText() {
diff --git a/src/main/java/ecdar/abstractions/DisplayableEdge.java b/src/main/java/ecdar/abstractions/DisplayableEdge.java
index aa93dc7f..52ced7b5 100644
--- a/src/main/java/ecdar/abstractions/DisplayableEdge.java
+++ b/src/main/java/ecdar/abstractions/DisplayableEdge.java
@@ -2,6 +2,7 @@
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;
@@ -27,8 +28,7 @@ public abstract class DisplayableEdge implements Nearable {
private final StringProperty update = new SimpleStringProperty("");
// Styling properties
- private final ObjectProperty color = new SimpleObjectProperty<>(Color.GREY_BLUE);
- private final ObjectProperty colorIntensity = new SimpleObjectProperty<>(Color.Intensity.I700);
+ private final ObjectProperty color = new SimpleObjectProperty<>(EnabledColor.getDefault());
private final ObservableList nails = FXCollections.observableArrayList();
// Circulars
@@ -109,30 +109,18 @@ public StringProperty updateProperty() {
return update;
}
- public Color getColor() {
+ public EnabledColor getColor() {
return color.get();
}
- public void setColor(final Color color) {
+ public void setColor(final EnabledColor color) {
this.color.set(color);
}
- public ObjectProperty colorProperty() {
+ public ObjectProperty colorProperty() {
return color;
}
- public Color.Intensity getColorIntensity() {
- return colorIntensity.get();
- }
-
- public void setColorIntensity(final Color.Intensity colorIntensity) {
- this.colorIntensity.set(colorIntensity);
- }
-
- public ObjectProperty colorIntensityProperty() {
- return colorIntensity;
- }
-
public void setIsHighlighted(final boolean highlight){ this.isHighlighted.set(highlight);}
public boolean getIsHighlighted(){ return this.isHighlighted.get(); }
diff --git a/src/main/java/ecdar/abstractions/EcdarModel.java b/src/main/java/ecdar/abstractions/EcdarModel.java
deleted file mode 100644
index c298e6ae..00000000
--- a/src/main/java/ecdar/abstractions/EcdarModel.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package ecdar.abstractions;
-
-/**
- * Ecdar graphical model.
- * This could be a component or a system.
- */
-public abstract class EcdarModel extends HighLevelModelObject {
-}
diff --git a/src/main/java/ecdar/abstractions/EcdarSystem.java b/src/main/java/ecdar/abstractions/EcdarSystem.java
index 35fe2f2b..d8fd96ae 100644
--- a/src/main/java/ecdar/abstractions/EcdarSystem.java
+++ b/src/main/java/ecdar/abstractions/EcdarSystem.java
@@ -1,8 +1,6 @@
package ecdar.abstractions;
-import ecdar.Ecdar;
import ecdar.utility.UndoRedoStack;
-import ecdar.utility.colors.Color;
import ecdar.utility.colors.EnabledColor;
import ecdar.utility.helpers.Boxed;
import com.google.gson.JsonArray;
@@ -21,8 +19,7 @@
* A model of a system.
* The class is called EcdarSystem, since Java already has a System class.
*/
-public class EcdarSystem extends EcdarModel implements Boxed {
- private static final String SYSTEM = "System";
+public class EcdarSystem extends HighLevelModel implements Boxed {
private static final String SYSTEM_ROOT_X = "systemRootX";
private static final String INSTANCES = "componentInstances";
private static final String OPERATORS = "operators";
@@ -32,15 +29,15 @@ public class EcdarSystem extends EcdarModel implements Boxed {
private final StringProperty description = new SimpleStringProperty("");
private final ObservableList componentInstances = FXCollections.observableArrayList();
private final ObservableList componentOperators = FXCollections.observableArrayList();
- private final ObservableList edges = FXCollections.observableArrayList();
+ private final ObservableList edges = FXCollections.observableArrayList();
private final SystemRoot systemRoot = new SystemRoot();
// Styling properties
private final Box box = new Box();
- public EcdarSystem() {
- setSystemName();
- setRandomColor();
+ public EcdarSystem(final EnabledColor color, final String name) {
+ this.nameProperty().set(name);
+ setColor(color);
getBox().setWidth(600d);
@@ -101,36 +98,32 @@ public void removeComponentOperator(final ComponentOperator operator) {
/* Edges */
- public ObservableList getEdges() {
+ public ObservableList getEdges() {
return edges;
}
- public void addEdge(final EcdarSystemEdge edge) {
+ public void addEdge(final SystemEdge edge) {
edges.add(edge);
}
- public void removeEdge(final EcdarSystemEdge edge) {
+ public void removeEdge(final SystemEdge edge) {
edges.remove(edge);
}
/**
* Dyes the system.
* @param color the color to dye with
- * @param intensity the intensity of the color
*/
- public void dye(final Color color, final Color.Intensity intensity) {
- final Color previousColor = colorProperty().get();
- final Color.Intensity previousColorIntensity = colorIntensityProperty().get();
+ public void dye(final EnabledColor color) {
+ final EnabledColor previousColor = colorProperty().get();
UndoRedoStack.pushAndPerform(() -> { // Perform
// Color the component
- setColorIntensity(intensity);
setColor(color);
}, () -> { // Undo
// Color the component
- setColorIntensity(previousColorIntensity);
setColor(previousColor);
- }, String.format("Changed the color of %s to %s", this, color.name()), "color-lens");
+ }, String.format("Changed the color of %s to %s", this, color.color.name()), "color-lens");
}
@Override
@@ -141,7 +134,7 @@ public JsonObject serialize() {
box.addProperties(result);
- result.addProperty(COLOR, EnabledColor.getIdentifier(getColor()));
+ result.addProperty(COLOR, EnabledColor.getIdentifier(getColor().color));
result.addProperty(SYSTEM_ROOT_X, systemRoot.getX());
@@ -170,8 +163,7 @@ public void deserialize(final JsonObject json) {
final EnabledColor enabledColor = EnabledColor.fromIdentifier(json.getAsJsonPrimitive(COLOR).getAsString());
if (enabledColor != null) {
- setColorIntensity(enabledColor.intensity);
- setColor(enabledColor.color);
+ setColor(enabledColor);
}
systemRoot.setX(json.getAsJsonPrimitive(SYSTEM_ROOT_X).getAsDouble());
@@ -187,19 +179,7 @@ public void deserialize(final JsonObject json) {
});
json.getAsJsonArray(EDGES).forEach(jsonEdge ->
- getEdges().add(new EcdarSystemEdge((JsonObject) jsonEdge, this)));
- }
-
- /**
- * Generate and sets a unique id for this system
- */
- public void setSystemName() {
- for(int counter = 1; ; counter++) {
- if(!Ecdar.getProject().getSystemNames().contains(SYSTEM + counter)){
- setName((SYSTEM + counter));
- return;
- }
- }
+ getEdges().add(new SystemEdge((JsonObject) jsonEdge, this)));
}
/**
@@ -207,8 +187,8 @@ public void setSystemName() {
* An edge is unfinished, if the has no target.
* @return The unfinished edge, or null if none was found.
*/
- public EcdarSystemEdge getUnfinishedEdge() {
- for (final EcdarSystemEdge edge : edges) {
+ public SystemEdge getUnfinishedEdge() {
+ for (final SystemEdge edge : edges) {
if (!edge.isFinished()) return edge;
}
return null;
diff --git a/src/main/java/ecdar/abstractions/Edge.java b/src/main/java/ecdar/abstractions/Edge.java
index 28f7cad8..754c9947 100644
--- a/src/main/java/ecdar/abstractions/Edge.java
+++ b/src/main/java/ecdar/abstractions/Edge.java
@@ -97,7 +97,7 @@ public BooleanProperty failingProperty() {
* @param component component to select a source and a target location within
* @return the edge
*/
- Edge cloneForVerification(final Component component) {
+ public Edge cloneForVerification(final Component component) {
final Edge clone = new Edge(component.findLocation(getSourceLocation().getId()), getStatus());
// Clone target location
diff --git a/src/main/java/ecdar/abstractions/GroupedEdge.java b/src/main/java/ecdar/abstractions/GroupedEdge.java
index 02503d9a..420762be 100644
--- a/src/main/java/ecdar/abstractions/GroupedEdge.java
+++ b/src/main/java/ecdar/abstractions/GroupedEdge.java
@@ -54,7 +54,6 @@ private void initializeFromEdge(Edge edge) {
setGuard(edge.getGuard());
setUpdate(edge.getUpdate());
setColor(edge.getColor());
- setColorIntensity(edge.getColorIntensity());
setIsHighlighted(edge.getIsHighlighted());
setIsLocked(edge.getIsLockedProperty().getValue());
setStatus(edge.getStatus());
@@ -116,7 +115,6 @@ public Edge getBaseSubEdge() {
edge.guardProperty().bind(this.guardProperty());
edge.updateProperty().bind(this.updateProperty());
edge.colorProperty().bind(this.colorProperty());
- edge.colorIntensityProperty().bind(this.colorIntensityProperty());
edge.setIsHighlighted(this.getIsHighlighted());
edge.getIsLockedProperty().bind(this.getIsLockedProperty());
edge.setGroup(this.getId());
diff --git a/src/main/java/ecdar/abstractions/HighLevelModel.java b/src/main/java/ecdar/abstractions/HighLevelModel.java
new file mode 100644
index 00000000..536955b4
--- /dev/null
+++ b/src/main/java/ecdar/abstractions/HighLevelModel.java
@@ -0,0 +1,78 @@
+package ecdar.abstractions;
+
+import ecdar.presentations.DropDownMenu;
+import ecdar.utility.colors.EnabledColor;
+import ecdar.utility.serialize.Serializable;
+import com.google.gson.JsonObject;
+import javafx.beans.property.*;
+
+/**
+ * An object used for verifications.
+ * This could be a component, a global declarations object, or a system.
+ */
+public abstract class HighLevelModel implements Serializable, DropDownMenu.HasColor {
+ private static final String NAME = "name";
+
+ static final String DECLARATIONS = "declarations";
+ public static final String DESCRIPTION = "description";
+ static final String COLOR = "color";
+
+ private final StringProperty name;
+ private final ObjectProperty color;
+ private boolean temporary = false;
+
+ public HighLevelModel() {
+ name = new SimpleStringProperty("");
+ color = new SimpleObjectProperty<>(EnabledColor.getDefault());
+ }
+
+ public String getName() {
+ return name.get();
+ }
+
+ public void setName(final String name) {
+ this.name.unbind();
+ this.name.set(name);
+ }
+
+ public StringProperty nameProperty() {
+ return name;
+ }
+
+ public EnabledColor getColor() {
+ return color.get();
+ }
+
+ /**
+ * Sets the color of the model
+ */
+ void setColor(final EnabledColor color) {
+ this.color.set(color);
+ }
+
+ public ObjectProperty colorProperty() {
+ return color;
+ }
+
+ public boolean isTemporary() {
+ return temporary;
+ }
+
+ public void setTemporary(final boolean newValue) {
+ this.temporary = newValue;
+ }
+
+ @Override
+ public JsonObject serialize() {
+ final JsonObject result = new JsonObject();
+
+ result.addProperty(NAME, getName());
+
+ return result;
+ }
+
+ @Override
+ public void deserialize(final JsonObject json) {
+ setName(json.getAsJsonPrimitive(NAME).getAsString());
+ }
+}
diff --git a/src/main/java/ecdar/abstractions/HighLevelModelObject.java b/src/main/java/ecdar/abstractions/HighLevelModelObject.java
deleted file mode 100644
index c1926c4e..00000000
--- a/src/main/java/ecdar/abstractions/HighLevelModelObject.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package ecdar.abstractions;
-
-import ecdar.Ecdar;
-import ecdar.presentations.DropDownMenu;
-import ecdar.utility.colors.Color;
-import ecdar.utility.colors.EnabledColor;
-import ecdar.utility.serialize.Serializable;
-import com.google.gson.JsonObject;
-import javafx.beans.property.*;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Random;
-
-/**
- * An object used for verifications.
- * This could be a component, a global declarations object, or a system.
- */
-public abstract class HighLevelModelObject implements Serializable, DropDownMenu.HasColor {
- private static final String NAME = "name";
-
- static final String DECLARATIONS = "declarations";
- public static final String DESCRIPTION = "description";
- static final String COLOR = "color";
-
- private final StringProperty name;
- private final ObjectProperty color;
- private final ObjectProperty colorIntensity;
- private boolean temporary = false;
-
- public HighLevelModelObject() {
- name = new SimpleStringProperty("");
- color = new SimpleObjectProperty<>(Color.GREY_BLUE);
- colorIntensity = new SimpleObjectProperty<>(Color.Intensity.I700);
- }
-
- public String getName() {
- return name.get();
- }
-
- public void setName(final String name) {
- this.name.unbind();
- this.name.set(name);
- }
-
- public StringProperty nameProperty() {
- return name;
- }
-
- public Color getColor() {
- return color.get();
- }
-
- public void setColor(final Color color) {
- this.color.set(color);
- }
-
- public ObjectProperty colorProperty() {
- return color;
- }
-
- public Color.Intensity getColorIntensity() {
- return colorIntensity.get();
- }
-
- public void setColorIntensity(final Color.Intensity colorIntensity) {
- this.colorIntensity.set(colorIntensity);
- }
-
- public ObjectProperty colorIntensityProperty() {
- return colorIntensity;
- }
-
- public boolean isTemporary() {
- return temporary;
- }
-
- public void setTemporary(final boolean newValue) {
- this.temporary = newValue;
- }
-
- /**
- * Sets a random color.
- * If some colors are not currently in use, choose among those.
- * Otherwise choose a between all available colors.
- */
- void setRandomColor() {
- // Color the new component in such a way that we avoid clashing with other components if possible
- final List availableColors = new ArrayList<>(EnabledColor.enabledColors);
- Ecdar.getProject().getComponents().forEach(component -> {
- availableColors.removeIf(enabledColor -> enabledColor.color.equals(component.getColor()));
- });
- if (availableColors.size() == 0) {
- availableColors.addAll(EnabledColor.enabledColors);
- }
- final int randomIndex = (new Random()).nextInt(availableColors.size());
- final EnabledColor selectedColor = availableColors.get(randomIndex);
- setColorIntensity(selectedColor.intensity);
- setColor(selectedColor.color);
- }
-
- @Override
- public JsonObject serialize() {
- final JsonObject result = new JsonObject();
-
- result.addProperty(NAME, getName());
-
- return result;
- }
-
- @Override
- public void deserialize(final JsonObject json) {
- setName(json.getAsJsonPrimitive(NAME).getAsString());
- }
-}
diff --git a/src/main/java/ecdar/abstractions/Location.java b/src/main/java/ecdar/abstractions/Location.java
index 632a06ba..8b5c458f 100644
--- a/src/main/java/ecdar/abstractions/Location.java
+++ b/src/main/java/ecdar/abstractions/Location.java
@@ -31,7 +31,6 @@ public class Location implements Circular, Serializable, Nearable, DropDownMenu.
private static final String INVARIANT_Y = "invariantY";
private static final String UNI = "U";
private static final String INC = "I";
- public static final String LOCATION = "L";
static final int ID_LETTER_LENGTH = 1;
// Verification properties
@@ -46,8 +45,7 @@ public class Location implements Circular, Serializable, Nearable, DropDownMenu.
private final DoubleProperty y = new SimpleDoubleProperty(0d);
private final DoubleProperty radius = new SimpleDoubleProperty(0d);
private final SimpleDoubleProperty scale = new SimpleDoubleProperty(1d);
- private final ObjectProperty color = new SimpleObjectProperty<>(Color.GREY_BLUE);
- private final ObjectProperty colorIntensity = new SimpleObjectProperty<>(Color.Intensity.I700);
+ private final ObjectProperty color = new SimpleObjectProperty<>(EnabledColor.getDefault());
private final DoubleProperty nicknameX = new SimpleDoubleProperty(0d);
private final DoubleProperty nicknameY = new SimpleDoubleProperty(0d);
@@ -66,19 +64,21 @@ public Location(final String id) {
setId(id);
}
- public Location(final Component component, final Type type, final double x, final double y){
+ public Location(final Component component, final Type type, final String id, final double x, final double y){
setX(x);
setY(y);
setType(type);
if(type == Type.UNIVERSAL){
setIsLocked(true);
- setId(UNI + component.generateUniIncId());
+ setId(UNI + id);
} else if (type == Type.INCONSISTENT) {
setIsLocked(true);
setUrgency(Location.Urgency.URGENT);
- setId(INC + component.generateUniIncId());
+ setId(INC + id);
+ } else {
+ setId(id);
}
- setColorIntensity(component.getColorIntensity());
+
setColor(component.getColor());
}
@@ -89,8 +89,8 @@ public Location(final JsonObject jsonObject) {
/**
* Generates an id for this, and binds reachability analysis.
*/
- public void initialize() {
- setId();
+ public void initialize(String id) {
+ setId(id);
}
/**
@@ -100,7 +100,7 @@ public void initialize() {
* Reachability analysis is not initialized.
* @return the clone
*/
- Location cloneForVerification() {
+ public Location cloneForVerification() {
final Location location = new Location();
location.setId(getId());
@@ -134,18 +134,6 @@ public String getId() {
return id.get();
}
- /**
- * Generate and sets a unique id for this location
- */
- private void setId() {
- for(int counter = 0; ; counter++) {
- if(!Ecdar.getProject().getLocationIds().contains(String.valueOf(counter))){
- id.set(LOCATION + counter);
- return;
- }
- }
- }
-
/**
* Sets a specific id for this location
* @param string id to set
@@ -219,30 +207,6 @@ public DoubleProperty yProperty() {
return y;
}
- public Color getColor() {
- return color.get();
- }
-
- public void setColor(final Color color) {
- this.color.set(color);
- }
-
- public ObjectProperty colorProperty() {
- return color;
- }
-
- public Color.Intensity getColorIntensity() {
- return colorIntensity.get();
- }
-
- public void setColorIntensity(final Color.Intensity colorIntensity) {
- this.colorIntensity.set(colorIntensity);
- }
-
- public ObjectProperty colorIntensityProperty() {
- return colorIntensity;
- }
-
public double getRadius() {
return radius.get();
}
@@ -318,6 +282,19 @@ public String getMostDescriptiveIdentifier() {
}
}
+ @Override
+ public ObjectProperty colorProperty() {
+ return color;
+ }
+
+ public void setColor(EnabledColor color) {
+ this.color.set(color);
+ }
+
+ public EnabledColor getColor() {
+ return color.get();
+ }
+
/**
* Adds an edge to the left side of a location
* @param syncString the string of the channel to synchronize over
@@ -399,7 +376,7 @@ public JsonObject serialize() {
result.addProperty(X, getX());
result.addProperty(Y, getY());
- result.addProperty(COLOR, EnabledColor.getIdentifier(getColor()));
+ result.addProperty(COLOR, EnabledColor.getIdentifier(getColor().color));
result.addProperty(NICKNAME_X, getNicknameX());
result.addProperty(NICKNAME_Y, getNicknameY());
@@ -420,11 +397,8 @@ public void deserialize(final JsonObject json) {
setX(json.getAsJsonPrimitive(X).getAsDouble());
setY(json.getAsJsonPrimitive(Y).getAsDouble());
- final EnabledColor enabledColor = (json.has(COLOR) ? EnabledColor.fromIdentifier(json.getAsJsonPrimitive(COLOR).getAsString()) : null);
- if (enabledColor != null) {
- setColorIntensity(enabledColor.intensity);
- setColor(enabledColor.color);
- }
+ final EnabledColor enabledColor = (json.has(COLOR) ? EnabledColor.fromIdentifier(json.getAsJsonPrimitive(COLOR).getAsString()) : EnabledColor.getDefault());
+ setColor(enabledColor);
if(json.has(NICKNAME_X) && json.has(NICKNAME_Y)) {
setNicknameX(json.getAsJsonPrimitive(NICKNAME_X).getAsDouble());
diff --git a/src/main/java/ecdar/abstractions/Project.java b/src/main/java/ecdar/abstractions/Project.java
index 738d8097..fbf599ce 100644
--- a/src/main/java/ecdar/abstractions/Project.java
+++ b/src/main/java/ecdar/abstractions/Project.java
@@ -21,6 +21,9 @@
* A project of models.
*/
public class Project {
+ public static final String LOCATION = "L";
+ public static final String COMPONENT = "Component";
+ public static final String SYSTEM = "System";
private final static String GLOBAL_DCL_FILENAME = "GlobalDeclarations";
private final static String QUERIES_FILENAME = "Queries";
private final static String JSON_FILENAME_EXTENSION = ".json";
@@ -56,7 +59,7 @@ public ObservableList getTempComponents() {
return tempComponents;
}
- public ObservableList getSystemsProperty() {
+ public ObservableList getSystems() {
return systems;
}
@@ -68,10 +71,95 @@ public Declarations getGlobalDeclarations() {
return globalDeclarations.get();
}
- public void setGlobalDeclarations(final Declarations declarations) {
+ private void setGlobalDeclarations(final Declarations declarations) {
globalDeclarations.set(declarations);
}
+ public void addComponent(Component newComponent) {
+ components.add(newComponent);
+ }
+
+ /**
+ * gets the id of all edges in the project and inserts it into a set
+ * @return the set of all edge ids
+ */
+ Set getEdgeIds(){
+ final Set ids = new HashSet<>();
+
+ for (final Component component : getComponents()) {
+ component.getEdges().forEach(edge -> ids.add(edge.getId().substring(Edge.ID_LETTER_LENGTH)));
+ }
+
+ return ids;
+ }
+
+ /**
+ * gets the id of all systems in the project and inserts it into a set
+ * @return the set of all system names
+ */
+ HashSet getSystemNames(){
+ final HashSet names = new HashSet<>();
+
+ for(final EcdarSystem system : getSystems()){
+ names.add(system.getName());
+ }
+
+ return names;
+ }
+
+ /**
+ * Gets a new GSON object.
+ * @return the GSON object
+ */
+ private static Gson getNewGson() {
+ return new GsonBuilder().setPrettyPrinting().create();
+ }
+
+ /**
+ * Gets a new file writer for saving a file.
+ * @param filename name of file without extension.
+ * @return the file writer
+ * @throws IOException if an IO error occurs
+ */
+ private static FileWriter getSaveFileWriter(final String filename) throws IOException {
+ return new FileWriter(Ecdar.projectDirectory.getValue() + File.separator + filename + ".json");
+ }
+
+ private static FileWriter getSaveFileWriter(final String filename, final String folderName) throws IOException {
+ return new FileWriter(Ecdar.projectDirectory.getValue() + File.separator + folderName + File.separator + filename + ".json");
+ }
+
+ /**
+ * Find a component by its name.
+ * @param name the name of the component looking for
+ * @return the component, or null if none is found
+ */
+ public Component findComponent(final String name) {
+ for (final Component component : getComponents()) {
+ if (component.getName().equals(name)) return component;
+ }
+
+ return null;
+ }
+
+ /**
+ * Cleans the project.
+ * Be sure to disable code analysis before call and enable after call.
+ */
+ public void clean() {
+ getGlobalDeclarations().clearDeclarationsText();
+
+ queries.clear();
+
+ components.clear();
+
+ tempComponents.clear();
+
+ systems.clear();
+
+ testPlans.clear();
+ }
+
/**
* Serializes and stores this as JSON files at a given directory.
* @param directory object containing path to the desired directory to store at
@@ -100,7 +188,7 @@ public void serialize(final File directory) throws IOException {
}
// Save systems
- for (final EcdarSystem system : getSystemsProperty()) {
+ for (final EcdarSystem system : getSystems()) {
final Writer writer = getSaveFileWriter(system.getName(), FOLDER_NAME_SYSTEMS);
getNewGson().toJson(system.serialize(), writer);
writer.close();
@@ -125,28 +213,6 @@ public void serialize(final File directory) throws IOException {
Ecdar.showToast("Project saved.");
}
- /**
- * Gets a new GSON object.
- * @return the GSON object
- */
- private static Gson getNewGson() {
- return new GsonBuilder().setPrettyPrinting().create();
- }
-
- /**
- * Gets a new file writer for saving a file.
- * @param filename name of file without extension.
- * @return the file writer
- * @throws IOException if an IO error occurs
- */
- private static FileWriter getSaveFileWriter(final String filename) throws IOException {
- return new FileWriter(Ecdar.projectDirectory.getValue() + File.separator + filename + ".json");
- }
-
- private static FileWriter getSaveFileWriter(final String filename, final String folderName) throws IOException {
- return new FileWriter(Ecdar.projectDirectory.getValue() + File.separator + folderName + File.separator + filename + ".json");
- }
-
/**
* Reads files in a folder and deserialize this based on the files and folders.
* @param projectFolder the folder where an Ecdar project are supposed to be
@@ -275,7 +341,7 @@ private void deserializeSystems(final File systemsFolder) throws IOException {
final List> list = new LinkedList<>(nameJsonMap.entrySet());
- list.sort(Comparator.comparing(Map.Entry::getKey));
+ list.sort(Map.Entry.comparingByKey());
final List orderedJsonSystems = new ArrayList<>();
@@ -287,7 +353,7 @@ private void deserializeSystems(final File systemsFolder) throws IOException {
Collections.reverse(orderedJsonSystems);
// Add the systems to the list
- orderedJsonSystems.forEach(json -> getSystemsProperty().add(new EcdarSystem(json)));
+ orderedJsonSystems.forEach(json -> getSystems().add(new EcdarSystem(json)));
}
/**
@@ -315,7 +381,7 @@ private void deserializeTestObjects(final File directory) throws IOException {
final List> list = new LinkedList<>(nameJsonMap.entrySet());
- list.sort(Comparator.comparing(Map.Entry::getKey));
+ list.sort(Map.Entry.comparingByKey());
final List orderedJsonSystems = new ArrayList<>();
@@ -329,114 +395,4 @@ private void deserializeTestObjects(final File directory) throws IOException {
// Add the test objects to the list
orderedJsonSystems.forEach(json -> getTestPlans().add(new MutationTestPlan(json)));
}
-
- /**
- * Resets components.
- * After this, there is only one component.
- * Be sure to disable code analysis before call and enable after call.
- */
- public void reset() {
- clean();
- components.add(new Component(true));
- }
-
- /**
- * Cleans the project.
- * Be sure to disable code analysis before call and enable after call.
- */
- public void clean() {
- getGlobalDeclarations().clearDeclarationsText();
-
- queries.clear();
-
- components.clear();
-
- tempComponents.clear();
-
- systems.clear();
-
- testPlans.clear();
- }
-
- /**
- * gets the id of all locations in the project and inserts it into a set
- * @return the set of all location ids
- */
- Set getLocationIds(){
- final Set ids = new HashSet<>();
-
- for (final Component component : getComponents()) {
- ids.addAll(component.getLocationIds());
- }
-
- return ids;
- }
-
- /**
- * gets the id of all edges in the project and inserts it into a set
- * @return the set of all edge ids
- */
- Set getEdgeIds(){
- final Set ids = new HashSet<>();
-
- for (final Component component : getComponents()) {
- ids.addAll(component.getEdgeIds());
- }
-
- return ids;
- }
-
- /**
- * gets the id of all systems in the project and inserts it into a set
- * @return the set of all system names
- */
- HashSet getSystemNames(){
- final HashSet names = new HashSet<>();
-
- for(final EcdarSystem system : getSystemsProperty()){
- names.add(system.getName());
- }
-
- return names;
- }
-
- /**
- * Gets universal/inconsistent ids for all components in the project
- * @return a set of universal/inconsistent ids
- */
- HashSet getUniIncIds() {
- final HashSet ids = new HashSet<>();
- for (final Component component : getComponents()){
- ids.add(component.getUniIncId());
- }
-
- return ids;
- }
-
- /**
- * Gets the name of all components in the project and inserts it into a set
- * @return the set of all component names
- */
- HashSet getComponentNames(){
- final HashSet names = new HashSet<>();
-
- for(final Component component : getComponents()){
- names.add(component.getName());
- }
-
- return names;
- }
-
- /**
- * Find a component by its name.
- * @param name the name of the component looking for
- * @return the component, or null if none is found
- */
- public Component findComponent(final String name) {
- for (final Component component : getComponents()) {
- if (component.getName().equals(name)) return component;
- }
-
- return null;
- }
}
diff --git a/src/main/java/ecdar/abstractions/Query.java b/src/main/java/ecdar/abstractions/Query.java
index b480cb07..175aa5d9 100644
--- a/src/main/java/ecdar/abstractions/Query.java
+++ b/src/main/java/ecdar/abstractions/Query.java
@@ -1,15 +1,23 @@
package ecdar.abstractions;
-import EcdarProtoBuf.ObjectProtos;
+import EcdarProtoBuf.QueryProtos;
+import com.google.gson.JsonParser;
import ecdar.Ecdar;
import ecdar.backend.*;
import ecdar.controllers.EcdarController;
+import ecdar.controllers.SimulatorController;
+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;
@@ -18,7 +26,7 @@ public class Query implements Serializable {
private static final String QUERY = "query";
private static final String COMMENT = "comment";
private static final String IS_PERIODIC = "isPeriodic";
- private static final String BACKEND = "backend";
+ private static final String ENGINE = "engine";
private final StringProperty query = new SimpleStringProperty("");
private final StringProperty comment = new SimpleStringProperty("");
@@ -26,7 +34,7 @@ public class Query implements Serializable {
private final SimpleBooleanProperty isPeriodic = new SimpleBooleanProperty(false);
private final ObjectProperty queryState = new SimpleObjectProperty<>(QueryState.UNKNOWN);
private final ObjectProperty type = new SimpleObjectProperty<>();
- private BackendInstance backend;
+ private Engine engine;
private final Consumer successConsumer = (aBoolean) -> {
@@ -100,11 +108,15 @@ public class Query implements Serializable {
}
};
- public Query(final String query, final String comment, final QueryState queryState) {
+ public Query(final String query, final String comment, final QueryState queryState, final Engine engine) {
this.setQuery(query);
this.comment.set(comment);
this.queryState.set(queryState);
- setBackend(BackendHelper.getDefaultBackendInstance());
+ setEngine(engine);
+ }
+
+ public Query(final String query, final String comment, final QueryState queryState) {
+ this(query, comment, queryState, BackendHelper.getDefaultEngine());
}
public Query(final JsonObject jsonElement) {
@@ -163,12 +175,12 @@ public void setIsPeriodic(final boolean isPeriodic) {
this.isPeriodic.set(isPeriodic);
}
- public BackendInstance getBackend() {
- return backend;
+ public Engine getEngine() {
+ return engine;
}
- public void setBackend(BackendInstance backend) {
- this.backend = backend;
+ public void setEngine(Engine engine) {
+ this.engine = engine;
}
public void setType(QueryType type) {
@@ -191,6 +203,198 @@ public Consumer getFailureConsumer() {
return failureConsumer;
}
+ /**
+ * Executes the query
+ */
+ public void execute() throws NoSuchElementException {
+ if (getQueryState().equals(QueryState.RUNNING) || !StringValidator.validateQuery(getQuery()))
+ return;
+
+ if (getQuery().isEmpty()) {
+ setQueryState(QueryState.SYNTAX_ERROR);
+ addError("Query is empty");
+ return;
+ }
+
+ setQueryState(QueryState.RUNNING);
+ errors().set("");
+
+ getEngine().enqueueQuery(this, this::handleQueryResponse, this::handleQueryBackendError);
+ }
+
+ private void handleQueryResponse(QueryProtos.QueryResponse value) {
+ if (getQueryState() == QueryState.UNKNOWN) return;
+ switch (value.getResultCase()) {
+ case REFINEMENT:
+ if (value.getRefinement().getSuccess()) {
+ setQueryState(QueryState.SUCCESSFUL);
+ getSuccessConsumer().accept(true);
+ } else {
+ setQueryState(QueryState.ERROR);
+ getFailureConsumer().accept(new BackendException.QueryErrorException(value.getRefinement().getReason()));
+ getSuccessConsumer().accept(false);
+ getStateActionConsumer().accept(value.getRefinement().getState(),
+ value.getRefinement().getActionList());
+ }
+ break;
+
+ case CONSISTENCY:
+ if (value.getConsistency().getSuccess()) {
+ setQueryState(QueryState.SUCCESSFUL);
+ getSuccessConsumer().accept(true);
+ } else {
+ setQueryState(QueryState.ERROR);
+ getFailureConsumer().accept(new BackendException.QueryErrorException(value.getConsistency().getReason()));
+ getSuccessConsumer().accept(false);
+ getStateActionConsumer().accept(value.getConsistency().getState(),
+ value.getConsistency().getActionList());
+ }
+ break;
+
+ case DETERMINISM:
+ if (value.getDeterminism().getSuccess()) {
+ setQueryState(QueryState.SUCCESSFUL);
+ getSuccessConsumer().accept(true);
+ } else {
+ setQueryState(QueryState.ERROR);
+ getFailureConsumer().accept(new BackendException.QueryErrorException(value.getDeterminism().getReason()));
+ getSuccessConsumer().accept(false);
+ getStateActionConsumer().accept(value.getDeterminism().getState(),
+ value.getDeterminism().getActionList());
+
+ }
+ break;
+
+ case IMPLEMENTATION:
+ if (value.getImplementation().getSuccess()) {
+ setQueryState(QueryState.SUCCESSFUL);
+ getSuccessConsumer().accept(true);
+ } else {
+ setQueryState(QueryState.ERROR);
+ getFailureConsumer().accept(new BackendException.QueryErrorException(value.getImplementation().getReason()));
+ getSuccessConsumer().accept(false);
+ //ToDo: These errors are not implemented in the Reveaal backend.
+ getStateActionConsumer().accept(value.getImplementation().getState(),
+ new ArrayList<>());
+ }
+ break;
+
+ case REACHABILITY:
+ if (value.getReachability().getSuccess()) {
+ setQueryState(QueryState.SUCCESSFUL);
+ Ecdar.showToast("Reachability check was successful and the location can be reached.");
+
+ //create list of edge id's
+ ArrayList edgeIds = new ArrayList<>();
+ for(var pathsList : value.getReachability().getComponentPathsList()){
+ for(var id : pathsList.getEdgeIdsList().toArray()) {
+ edgeIds.add(id.toString());
+ }
+ }
+ //highlight the edges
+ SimulatorController.getSimulationHandler().highlightReachabilityEdges(edgeIds);
+ getSuccessConsumer().accept(true);
+ }
+ else if(!value.getReachability().getSuccess()){
+ Ecdar.showToast("Reachability check was successful but the location cannot be reached.");
+ getSuccessConsumer().accept(true);
+ } else {
+ setQueryState(QueryState.ERROR);
+ Ecdar.showToast("Error from backend: Reachability check was unsuccessful!");
+ getFailureConsumer().accept(new BackendException.QueryErrorException(value.getReachability().getReason()));
+ getSuccessConsumer().accept(false);
+ //ToDo: These errors are not implemented in the Reveaal backend.
+ getStateActionConsumer().accept(value.getReachability().getState(),
+ new ArrayList<>());
+ }
+ break;
+
+ case COMPONENT:
+ setQueryState(QueryState.SUCCESSFUL);
+ getSuccessConsumer().accept(true);
+ JsonObject returnedComponent = (JsonObject) JsonParser.parseString(value.getComponent().getComponent().getJson());
+ addGeneratedComponent(new Component(returnedComponent));
+ break;
+
+ case ERROR:
+ setQueryState(QueryState.ERROR);
+ Ecdar.showToast(value.getError());
+ getFailureConsumer().accept(new BackendException.QueryErrorException(value.getError()));
+ getSuccessConsumer().accept(false);
+ break;
+
+ case RESULT_NOT_SET:
+ setQueryState(QueryState.ERROR);
+ getSuccessConsumer().accept(false);
+ break;
+ }
+ }
+
+ 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];
+
+ if ("DEADLINE_EXCEEDED".equals(errorType)) {
+ setQueryState(QueryState.ERROR);
+ getFailureConsumer().accept(new BackendException.QueryErrorException("The engine did not answer the request in time"));
+ } else {
+ try {
+ setQueryState(QueryState.ERROR);
+ getFailureConsumer().accept(new BackendException.QueryErrorException("The execution of this query failed with message:" + System.lineSeparator() + t.getLocalizedMessage()));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void addGeneratedComponent(Component newComponent) {
+ Platform.runLater(() -> {
+ newComponent.setTemporary(true);
+
+ ObservableList listOfGeneratedComponents = Ecdar.getProject().getTempComponents(); // ToDo NIELS: Refactor
+ Component matchedComponent = null;
+
+ for (Component currentGeneratedComponent : listOfGeneratedComponents) {
+ int comparisonOfNames = currentGeneratedComponent.getName().compareTo(newComponent.getName());
+
+ if (comparisonOfNames == 0) {
+ matchedComponent = currentGeneratedComponent;
+ break;
+ } else if (comparisonOfNames < 0) {
+ break;
+ }
+ }
+
+ if (matchedComponent == null) {
+ UndoRedoStack.pushAndPerform(() -> { // Perform
+ Ecdar.getProject().getTempComponents().add(newComponent);
+ }, () -> { // Undo
+ Ecdar.getProject().getTempComponents().remove(newComponent);
+ }, "Created new component: " + newComponent.getName(), "add-circle");
+ } else {
+ // Remove current component with name and add the newly generated one
+ Component finalMatchedComponent = matchedComponent;
+ UndoRedoStack.pushAndPerform(() -> { // Perform
+ Ecdar.getProject().getTempComponents().remove(finalMatchedComponent);
+ Ecdar.getProject().getTempComponents().add(newComponent);
+ }, () -> { // Undo
+ Ecdar.getProject().getTempComponents().remove(newComponent);
+ Ecdar.getProject().getTempComponents().add(finalMatchedComponent);
+ }, "Created new component: " + newComponent.getName(), "add-circle");
+ }
+
+ Ecdar.getProject().addComponent(newComponent);
+ });
+ }
+
/**
* Getter for the state action consumer.
*
@@ -200,7 +404,6 @@ public BiConsumer> getStateActionConsumer() {
return stateActionConsumer;
}
-
@Override
public JsonObject serialize() {
final JsonObject result = new JsonObject();
@@ -208,7 +411,7 @@ public JsonObject serialize() {
result.addProperty(QUERY, getType().getQueryName() + ": " + getQuery());
result.addProperty(COMMENT, getComment());
result.addProperty(IS_PERIODIC, isPeriodic());
- result.addProperty(BACKEND, backend.getName());
+ result.addProperty(ENGINE, engine.getName());
return result;
}
@@ -231,10 +434,10 @@ public void deserialize(final JsonObject json) {
setIsPeriodic(json.getAsJsonPrimitive(IS_PERIODIC).getAsBoolean());
}
- if (json.has(BACKEND)) {
- setBackend(BackendHelper.getBackendInstanceByName(json.getAsJsonPrimitive(BACKEND).getAsString()));
+ if(json.has(ENGINE)) {
+ setEngine(BackendHelper.getEngineByName(json.getAsJsonPrimitive(ENGINE).getAsString()));
} else {
- setBackend(BackendHelper.getDefaultBackendInstance());
+ setEngine(BackendHelper.getDefaultEngine());
}
}
diff --git a/src/main/java/ecdar/abstractions/QueryType.java b/src/main/java/ecdar/abstractions/QueryType.java
index 0ea4b1f2..4552cffb 100644
--- a/src/main/java/ecdar/abstractions/QueryType.java
+++ b/src/main/java/ecdar/abstractions/QueryType.java
@@ -6,10 +6,10 @@ public enum QueryType {
QUOTIENT("quotient", "\\"),
SPECIFICATION("specification", "Spec"),
IMPLEMENTATION("implementation", "Imp"),
- LOCAL_CONSISTENCY("consistency", "lCon"), // ToDo NIELS: Will become local-consistency
- GLOBAL_CONSISTENCY("global-consistency", "gCon"),
- BISIM_MIN("bisim", "bsim"),
- GET_COMPONENT("get-component", "get");
+ CONSISTENCY("consistency", "Con"),
+ LOCAL_CONSISTENCY("local-consistency", "LCon"),
+ BISIM_MINIM("bisim-minim", "Bsim"),
+ GET_COMPONENT("get-component", "Get");
private final String queryName;
private final String symbol;
@@ -29,26 +29,23 @@ public String getSymbol() {
public static QueryType fromString(String s) {
switch (s.toLowerCase()) {
+ case "reachability":
+ return REACHABILITY;
case "refinement":
return REFINEMENT;
- case "quotient":
- return QUOTIENT;
case "specification":
return SPECIFICATION;
case "implementation":
return IMPLEMENTATION;
case "consistency":
+ return CONSISTENCY;
case "local-consistency":
return LOCAL_CONSISTENCY;
- case "global-consistency":
- return GLOBAL_CONSISTENCY;
- case "bisim":
- return BISIM_MIN;
+ case "bisim-minim":
+ return BISIM_MINIM;
case "get":
case "get-component":
return GET_COMPONENT;
- case "reachability":
- return REACHABILITY;
default:
return null;
}
diff --git a/src/main/java/ecdar/abstractions/SimpleComponentsSystemDeclarations.java b/src/main/java/ecdar/abstractions/SimpleComponentsSystemDeclarations.java
deleted file mode 100644
index 190f9352..00000000
--- a/src/main/java/ecdar/abstractions/SimpleComponentsSystemDeclarations.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package ecdar.abstractions;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Systems declarations for some components.
- * The generation text will be generated.
- */
-public class SimpleComponentsSystemDeclarations extends Declarations {
- public SimpleComponentsSystemDeclarations(final Component ... components) {
- super("SimpleComponentsSystemDeclarations");
-
- if (components.length < 1) throw new IllegalArgumentException("Cannot contruct declarations without any components");
-
- final List names = new ArrayList<>();
- for (final Component component : components) names.add(component.getName());
-
- final StringBuilder declarationsText = new StringBuilder("system " + String.join(", ", names) + ";\n");
-
- // Add inputs and outputs of components to declarations
- for (final Component component : components) {
- final List comp1Io = new ArrayList<>();
- for (final String inputSync : component.getInputStrings()) comp1Io.add(inputSync + "?");
- for (final String outputSync : component.getOutputStrings()) comp1Io.add(outputSync + "!");
- declarationsText.append("IO ").append(component.getName()).append(" {").append(String.join(", ", comp1Io)).append("}\n");
- }
-
- setDeclarationsText(declarationsText.toString());
- }
-}
diff --git a/src/main/java/ecdar/abstractions/EcdarSystemEdge.java b/src/main/java/ecdar/abstractions/SystemEdge.java
similarity index 92%
rename from src/main/java/ecdar/abstractions/EcdarSystemEdge.java
rename to src/main/java/ecdar/abstractions/SystemEdge.java
index 503e4831..0f91d139 100644
--- a/src/main/java/ecdar/abstractions/EcdarSystemEdge.java
+++ b/src/main/java/ecdar/abstractions/SystemEdge.java
@@ -10,9 +10,8 @@
/**
* This is an edge in an Ecdar system.
- * The class is called EcdarSystemEdge, since there is already an SystemEdge in com.uppaal.model.system.
*/
-public class EcdarSystemEdge {
+public class SystemEdge {
private static final String CHILD = "child";
private static final String PARENT = "parent";
// Verification properties
@@ -26,7 +25,7 @@ public class EcdarSystemEdge {
* Constructor
* @param node A node of this edge. This can either be a child or a parent
*/
- public EcdarSystemEdge(final SystemElement node) {
+ public SystemEdge(final SystemElement node) {
tempNode.set(node);
}
@@ -35,7 +34,7 @@ public EcdarSystemEdge(final SystemElement node) {
* @param json the JSON object
* @param system system containing the edge
*/
- public EcdarSystemEdge(final JsonObject json, final EcdarSystem system) {
+ public SystemEdge(final JsonObject json, final EcdarSystem system) {
deserialize(json, system);
}
diff --git a/src/main/java/ecdar/backend/BackendConnection.java b/src/main/java/ecdar/backend/BackendConnection.java
deleted file mode 100644
index 5bd19550..00000000
--- a/src/main/java/ecdar/backend/BackendConnection.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package ecdar.backend;
-
-import EcdarProtoBuf.EcdarBackendGrpc;
-import io.grpc.ManagedChannel;
-
-import java.util.concurrent.TimeUnit;
-
-public class BackendConnection {
- private final Process process;
- private final EcdarBackendGrpc.EcdarBackendStub stub;
- private final ManagedChannel channel;
- private final BackendInstance backendInstance;
-
- BackendConnection(BackendInstance backendInstance, Process process, EcdarBackendGrpc.EcdarBackendStub stub, ManagedChannel channel) {
- this.process = process;
- this.backendInstance = backendInstance;
- this.stub = stub;
- this.channel = channel;
- }
-
- /**
- * Get the gRPC stub of the connection to use for query execution
- *
- * @return the gRPC stub of this connection
- */
- public EcdarBackendGrpc.EcdarBackendStub getStub() {
- return stub;
- }
-
- /**
- * Get the backend instance that should be used to execute
- * the query currently associated with this backend connection
- *
- * @return the instance of the associated executable query object,
- * or null, if no executable query is currently associated
- */
- public BackendInstance getBackendInstance() {
- return backendInstance;
- }
-
- /**
- * Close the gRPC connection and end the process
- */
- public void close() {
- if (!channel.isShutdown()) {
- try {
- channel.shutdown();
- if (!channel.awaitTermination(45, TimeUnit.SECONDS)) {
- channel.shutdownNow(); // Forcefully close the connection
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- // If the backend-instance is remote, there will not be a process
- if (process != null) {
- process.destroy();
- }
- }
-}
diff --git a/src/main/java/ecdar/backend/BackendDriver.java b/src/main/java/ecdar/backend/BackendDriver.java
deleted file mode 100644
index 6b05988d..00000000
--- a/src/main/java/ecdar/backend/BackendDriver.java
+++ /dev/null
@@ -1,184 +0,0 @@
-package ecdar.backend;
-
-import EcdarProtoBuf.EcdarBackendGrpc;
-import ecdar.Ecdar;
-import io.grpc.*;
-import org.springframework.util.SocketUtils;
-
-import java.io.*;
-import java.util.*;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.TimeUnit;
-
-public class BackendDriver {
- private final BlockingQueue requestQueue = new ArrayBlockingQueue<>(200);
- private final Map> openBackendConnections = new HashMap<>();
- private final int responseDeadline = 20000;
- private final int rerunRequestDelay = 200;
- private final int numberOfRetriesPerQuery = 5;
-
- public BackendDriver() {
- // ToDo NIELS: Consider multiple consumer threads using 'for(int i = 0; i < x; i++) {}'
- GrpcRequestConsumer consumer = new GrpcRequestConsumer();
- Thread consumerThread = new Thread(consumer);
- consumerThread.start();
- }
-
- public int getResponseDeadline() {
- return responseDeadline;
- }
-
- /**
- * Add a GrpcRequest to the request queue to be executed when a backend is available
- *
- * @param request The GrpcRequest to be executed later
- */
- public void addRequestToExecutionQueue(GrpcRequest request) {
- requestQueue.add(request);
- }
-
- public void addBackendConnection(BackendConnection backendConnection) {
- var relatedQueue = this.openBackendConnections.get(backendConnection.getBackendInstance());
- if (!relatedQueue.contains(backendConnection)) relatedQueue.add(backendConnection);
- }
-
- /**
- * Filters the list of open {@link BackendConnection}s to the specified {@link BackendInstance} and returns the
- * first match or attempts to start a new connection if none is found.
- *
- * @param backend backend instance to get a connection to (e.g. Reveaal, j-Ecdar, custom_engine)
- * @return a BackendConnection object linked to the backend, either from the open backend connection list
- * or a newly started connection.
- * @throws BackendException.NoAvailableBackendConnectionException if unable to retrieve a connection to the backend
- * and unable to start a new one
- */
- private BackendConnection getBackendConnection(BackendInstance backend) throws BackendException.NoAvailableBackendConnectionException {
- BackendConnection connection;
- try {
- if (!openBackendConnections.containsKey(backend))
- openBackendConnections.put(backend, new ArrayBlockingQueue<>(backend.getNumberOfInstances() + 1));
-
- // If no open connection is free, attempt to start a new one
- if (openBackendConnections.get(backend).size() < 1) {
- tryStartNewBackendConnection(backend);
- }
-
- if (backend.isThreadSafe()){
- connection = openBackendConnections.get(backend).peek();
- }
- else{
- // Block until a connection becomes available
- connection = openBackendConnections.get(backend).take();
- }
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- return connection;
- }
-
- /**
- * Close all open backend connection and kill all locally running processes
- *
- * @throws IOException if any of the sockets do not respond
- */
- public void closeAllBackendConnections() throws IOException {
- for (BlockingQueue bq : openBackendConnections.values()) {
- for (BackendConnection bc : bq) bc.close();
- }
- }
-
- /**
- * Attempts to start a new connection to the specified backend. On success, the backend is added to the associated
- * queue, otherwise, nothing happens.
- *
- * @param backend the target backend for the connection
- */
- private void tryStartNewBackendConnection(BackendInstance backend) {
- Process p = null;
- String hostAddress = (backend.isLocal() ? "127.0.0.1" : backend.getBackendLocation());
- long portNumber = 0;
-
- if (backend.isLocal()) {
- try {
- portNumber = SocketUtils.findAvailableTcpPort(backend.getPortStart(), backend.getPortEnd());
- } catch (IllegalStateException e) {
- // No port was available in range, we assume that connections are running on all ports
- return;
- }
-
- do {
- ProcessBuilder pb = new ProcessBuilder(backend.getBackendLocation(), "-p", hostAddress + ":" + portNumber);
-
- try {
- p = pb.start();
- } catch (IOException ioException) {
- Ecdar.showToast("Unable to start local backend instance");
- ioException.printStackTrace();
- return;
- }
- // If the process is not alive, it failed while starting up, try again
- } while (!p.isAlive());
- } else {
- // Filter open connections to this backend and map their used ports to an int stream
- var activeEnginePorts = openBackendConnections.get(backend).stream()
- .mapToInt((bi) -> Integer.parseInt(bi.getStub().getChannel().authority().split(":", 2)[1]));
-
- int currentPort = backend.getPortStart();
- do {
- // Find port not already connected to
- int tempPortNumber = currentPort;
- if (activeEnginePorts.noneMatch((i) -> i == tempPortNumber)) {
- portNumber = tempPortNumber;
- } else {
- currentPort++;
- }
- } while (portNumber == 0 && currentPort <= backend.getPortEnd());
-
- if (currentPort > backend.getPortEnd()) {
- Ecdar.showToast("Unable to connect to remote engine: " + backend.getName() + " within port range " + backend.getPortStart() + " - " + backend.getPortEnd());
- return;
- }
- }
-
- ManagedChannel channel = ManagedChannelBuilder.forTarget(hostAddress + ":" + portNumber)
- .usePlaintext()
- .keepAliveTime(1000, TimeUnit.MILLISECONDS)
- .build();
-
- EcdarBackendGrpc.EcdarBackendStub stub = EcdarBackendGrpc.newStub(channel);
- BackendConnection newConnection = new BackendConnection(backend, p, stub, channel);
- addBackendConnection(newConnection);
- }
-
- private class GrpcRequestConsumer implements Runnable {
- @Override
- public void run() {
- while (true) {
- try {
- GrpcRequest request = requestQueue.take();
-
- try {
- request.tries++;
- request.execute(getBackendConnection(request.getBackend()));
- } catch (BackendException.NoAvailableBackendConnectionException e) {
- e.printStackTrace();
- if (request.tries < numberOfRetriesPerQuery) {
- new Timer().schedule(new TimerTask() {
- @Override
- public void run() {
- requestQueue.add(request);
- }
- }, rerunRequestDelay);
- } else {
- Ecdar.showToast("Unable to find a connection to the requested engine");
- }
- return;
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
-}
diff --git a/src/main/java/ecdar/backend/BackendException.java b/src/main/java/ecdar/backend/BackendException.java
index 7c77a582..db66a21c 100644
--- a/src/main/java/ecdar/backend/BackendException.java
+++ b/src/main/java/ecdar/backend/BackendException.java
@@ -9,12 +9,32 @@ public BackendException(final String message, final Throwable cause) {
super(message, cause);
}
- public static class NoAvailableBackendConnectionException extends BackendException {
- public NoAvailableBackendConnectionException(final String message) {
+ public static class NoAvailableEngineConnectionException extends BackendException {
+ public NoAvailableEngineConnectionException(final String message) {
super(message);
}
- public NoAvailableBackendConnectionException(final String message, final Throwable cause) {
+ public NoAvailableEngineConnectionException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ public static class gRpcChannelShutdownException extends BackendException {
+ public gRpcChannelShutdownException(final String message) {
+ super(message);
+ }
+
+ public gRpcChannelShutdownException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ public static class EngineProcessDestructionException extends BackendException {
+ public EngineProcessDestructionException(final String message) {
+ super(message);
+ }
+
+ public EngineProcessDestructionException(final String message, final Throwable cause) {
super(message, cause);
}
}
diff --git a/src/main/java/ecdar/backend/BackendHelper.java b/src/main/java/ecdar/backend/BackendHelper.java
index 442e0578..a47c7718 100644
--- a/src/main/java/ecdar/backend/BackendHelper.java
+++ b/src/main/java/ecdar/backend/BackendHelper.java
@@ -3,7 +3,6 @@
import EcdarProtoBuf.ComponentProtos;
import ecdar.Ecdar;
import ecdar.abstractions.*;
-import ecdar.simulation.SimulationState;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -22,9 +21,9 @@
public final class BackendHelper {
final static String TEMP_DIRECTORY = "temporary";
- private static BackendInstance defaultBackend = null;
- private static ObservableList backendInstances = new SimpleListProperty<>();
- private static List backendInstancesUpdatedListeners = new ArrayList<>();
+ private static Engine defaultEngine = null;
+ private static ObservableList engines = new SimpleListProperty<>();
+ private static final List enginesUpdatedListeners = new ArrayList<>();
/**
* Stores a query as a backend XML query file in the "temporary" directory.
@@ -59,185 +58,85 @@ public static String getTempDirectoryAbsolutePath() throws URISyntaxException {
}
/**
- * Stop all running queries.
+ * Clears all queued queries, stops all active engines, and closes all open engine connections
*/
- public static void stopQueries() {
- Ecdar.getProject().getQueries().forEach(Query::cancel);
- }
-
- public static String getLocationReachableQuery(final Location endLocation, final Component component, final String query) {
- return getLocationReachableQuery(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 getLocationReachableQuery(final Location endLocation, final Component component, final String query, final SimulationState state) {
- var stringBuilder = new StringBuilder();
-
- // append simulation query
- stringBuilder.append(query);
-
- // append arrow
- stringBuilder.append(" -> ");
-
- // append start location here TODO
- if (state != null){
- stringBuilder.append(getStartStateString(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();
- }
-
- private static String getStartStateString(SimulationState state) {
- var stringBuilder = new StringBuilder();
-
- // append locations
- var locations = state.getLocations();
- stringBuilder.append("[");
- var appendLocationWithSeparator = false;
- for(var componentName:Ecdar.getSimulationHandler().getComponentsInSimulation()){
- var locationFound = false;
-
- for(var location:locations){
- if (location.getKey().equals(componentName)){
- if (appendLocationWithSeparator){
- stringBuilder.append("," + location.getValue());
- }
- else{
- stringBuilder.append(location.getValue());
- }
- locationFound = true;
- }
- if (locationFound){
- // don't go through more locations, when a location is found for the specific component that we're looking at
- break;
- }
+ public static void clearEngineConnections() throws BackendException {
+ BackendHelper.stopQueries();
+
+ BackendException exception = new BackendException("Exceptions were thrown while attempting to close engine connections");
+ for (Engine engine : engines) {
+ try {
+ engine.closeConnections();
+ } catch (BackendException e) {
+ exception.addSuppressed(e);
}
- appendLocationWithSeparator = true;
}
- stringBuilder.append("]");
-
- // append clock values
- var clocks = state.getSimulationClocks();
- stringBuilder.append("()");
- return stringBuilder.toString();
- }
-
- private static String getEndStateString(String componentName, String endLocationId) {
- var stringBuilder = new StringBuilder();
-
- stringBuilder.append("[");
- var appendLocationWithSeparator = false;
-
- for (var component:Ecdar.getSimulationHandler().getComponentsInSimulation())
- {
- if (component.equals(componentName)){
- if (appendLocationWithSeparator){
- stringBuilder.append("," + endLocationId);
- }
- else{
- stringBuilder.append(endLocationId);
- }
- }
- else{ // add underscore to indicate, that we don't care about the end locations in the other components
- if (appendLocationWithSeparator){
- stringBuilder.append(",_");
- }
- else{
- stringBuilder.append("_");
- }
- }
- if (!appendLocationWithSeparator) {
- appendLocationWithSeparator = true;
- }
+ if (exception.getSuppressed().length > 0) {
+ throw exception;
}
- stringBuilder.append("]");
- stringBuilder.append("()");
-
- return stringBuilder.toString();
}
/**
- * 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
+ * Stop all running queries.
*/
- 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";
+ public static void stopQueries() {
+ Ecdar.getProject().getQueries().forEach(Query::cancel);
}
/**
- * Returns the BackendInstance with the specified name, or null, if no such BackendInstance exists
+ * Returns the Engine with the specified name, or null, if no such Engine exists
*
- * @param backendInstanceName Name of the BackendInstance to return
- * @return The BackendInstance with matching name
- * or the default backend instance, if no matching backendInstance exists
+ * @param engineName Name of the Engine to return
+ * @return The Engine with matching name
+ * or the default engine, if no matching engine exists
*/
- public static BackendInstance getBackendInstanceByName(String backendInstanceName) {
- Optional backendInstance = BackendHelper.backendInstances.stream().filter(bi -> bi.getName().equals(backendInstanceName)).findFirst();
- return backendInstance.orElse(BackendHelper.getDefaultBackendInstance());
+ public static Engine getEngineByName(String engineName) {
+ Optional engine = BackendHelper.engines.stream().filter(e -> e.getName().equals(engineName)).findFirst();
+ return engine.orElse(BackendHelper.getDefaultEngine());
}
/**
- * Returns the default BackendInstance
+ * Returns the default Engine
*
- * @return The default BackendInstance
+ * @return The default Engine
*/
- public static BackendInstance getDefaultBackendInstance() {
- return defaultBackend;
+ public static Engine getDefaultEngine() {
+ return defaultEngine;
}
/**
- * Sets the list of BackendInstances to match the provided list
+ * Sets the list of engines to match the provided list
*
- * @param updatedBackendInstances The list of BackendInstances that should be stored
+ * @param updatedEngines The list of engines that should be stored
*/
- public static void updateBackendInstances(ArrayList updatedBackendInstances) {
- BackendHelper.backendInstances = FXCollections.observableList(updatedBackendInstances);
- for (Runnable runnable : BackendHelper.backendInstancesUpdatedListeners) {
+ public static void updateEngineInstances(ArrayList updatedEngines) {
+ BackendHelper.engines = FXCollections.observableList(updatedEngines);
+ for (Runnable runnable : BackendHelper.enginesUpdatedListeners) {
runnable.run();
}
}
/**
- * Returns the ObservableList of BackendInstances
+ * Returns the ObservableList of engines
*
- * @return The ObservableList of BackendInstances
+ * @return The ObservableList of engines
*/
- public static ObservableList getBackendInstances() {
- return BackendHelper.backendInstances;
+ public static ObservableList getEngines() {
+ return BackendHelper.engines;
}
/**
- * Sets the default BackendInstance to the provided object
+ * Sets the default Engine to the provided object
*
- * @param newDefaultBackend The new defaultBackend
+ * @param newDefaultEngine The new default engine
*/
- public static void setDefaultBackendInstance(BackendInstance newDefaultBackend) {
- BackendHelper.defaultBackend = newDefaultBackend;
+ public static void setDefaultEngine(Engine newDefaultEngine) {
+ BackendHelper.defaultEngine = newDefaultEngine;
}
- public static void addBackendInstanceListener(Runnable runnable) {
- BackendHelper.backendInstancesUpdatedListeners.add(runnable);
+ public static void addEngineInstanceListener(Runnable runnable) {
+ BackendHelper.enginesUpdatedListeners.add(runnable);
}
public static ComponentProtos.ComponentsInfo.Builder getComponentsInfoBuilder(String query) {
diff --git a/src/main/java/ecdar/backend/BackendInstance.java b/src/main/java/ecdar/backend/BackendInstance.java
deleted file mode 100644
index feb66ca9..00000000
--- a/src/main/java/ecdar/backend/BackendInstance.java
+++ /dev/null
@@ -1,137 +0,0 @@
-package ecdar.backend;
-
-import com.google.gson.JsonObject;
-import ecdar.utility.serialize.Serializable;
-import javafx.beans.property.SimpleBooleanProperty;
-
-public class BackendInstance implements Serializable {
- private static final String NAME = "name";
- private static final String IS_LOCAL = "isLocal";
- private static final String IS_DEFAULT = "isDefault";
- private static final String LOCATION = "location";
- private static final String PORT_RANGE_START = "portRangeStart";
- private static final String PORT_RANGE_END = "portRangeEnd";
- private static final String LOCKED = "locked";
- private static final String IS_THREAD_SAFE = "isThreadSafe";
-
- private String name;
- private boolean isLocal;
- private boolean isDefault;
- private boolean isThreadSafe;
- private String backendLocation;
- private int portStart;
- private int portEnd;
- private SimpleBooleanProperty locked = new SimpleBooleanProperty(false);
-
- public BackendInstance() {};
-
- public BackendInstance(final JsonObject jsonObject) {
- deserialize(jsonObject);
- };
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public boolean isLocal() {
- return isLocal;
- }
-
- public void setLocal(boolean local) {
- isLocal = local;
- }
-
- public boolean isDefault() {
- return isDefault;
- }
-
- public void setDefault(boolean aDefault) {
- isDefault = aDefault;
- }
-
- public boolean isThreadSafe() {
- return isThreadSafe;
- }
-
- public void setIsThreadSafe(boolean threadSafe) {
- isThreadSafe = threadSafe;
- }
-
- public String getBackendLocation() {
- return backendLocation;
- }
-
- public void setBackendLocation(String backendLocation) {
- this.backendLocation = backendLocation;
- }
-
- public int getPortStart() {
- return portStart;
- }
-
- public void setPortStart(int portStart) {
- this.portStart = portStart;
- }
-
- public int getPortEnd() {
- return portEnd;
- }
-
- public void setPortEnd(int portEnd) {
- this.portEnd = portEnd;
- }
-
- public int getNumberOfInstances() {
- return this.portEnd - this.portStart;
- }
-
- public void lockInstance() {
- locked.set(true);
- }
-
- public SimpleBooleanProperty getLockedProperty() {
- return locked;
- }
-
- @Override
- public JsonObject serialize() {
- final JsonObject result = new JsonObject();
- result.addProperty(NAME, getName());
- result.addProperty(IS_LOCAL, isLocal());
- result.addProperty(IS_DEFAULT, isDefault());
- result.addProperty(IS_THREAD_SAFE, isThreadSafe());
- result.addProperty(LOCATION, getBackendLocation());
- result.addProperty(PORT_RANGE_START, getPortStart());
- result.addProperty(PORT_RANGE_END, getPortEnd());
- result.addProperty(LOCKED, getLockedProperty().get());
-
- return result;
- }
-
- @Override
- public void deserialize(final JsonObject json) {
- setName(json.getAsJsonPrimitive(NAME).getAsString());
- setLocal(json.getAsJsonPrimitive(IS_LOCAL).getAsBoolean());
- setDefault(json.getAsJsonPrimitive(IS_DEFAULT).getAsBoolean());
-
- try { // ToDo NIELS: Decide to either do this or simply reload defaults
- setIsThreadSafe(json.getAsJsonPrimitive(IS_THREAD_SAFE).getAsBoolean());
- } catch (NullPointerException e) {
- setIsThreadSafe(false);
- }
-
- setBackendLocation(json.getAsJsonPrimitive(LOCATION).getAsString());
- setPortStart(json.getAsJsonPrimitive(PORT_RANGE_START).getAsInt());
- setPortEnd(json.getAsJsonPrimitive(PORT_RANGE_END).getAsInt());
- if (json.getAsJsonPrimitive(LOCKED).getAsBoolean()) lockInstance();
- }
-
- @Override
- public String toString() {
- return name;
- }
-}
diff --git a/src/main/java/ecdar/backend/Engine.java b/src/main/java/ecdar/backend/Engine.java
new file mode 100644
index 00000000..936178e9
--- /dev/null
+++ b/src/main/java/ecdar/backend/Engine.java
@@ -0,0 +1,337 @@
+package ecdar.backend;
+
+import EcdarProtoBuf.QueryProtos;
+import com.google.gson.JsonObject;
+import ecdar.Ecdar;
+import ecdar.abstractions.Query;
+import ecdar.utility.serialize.Serializable;
+import io.grpc.stub.StreamObserver;
+import javafx.beans.property.SimpleBooleanProperty;
+
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.function.Consumer;
+
+public class Engine implements Serializable {
+ private static final String NAME = "name";
+ private static final String IS_LOCAL = "isLocal";
+ private static final String IS_DEFAULT = "isDefault";
+ private static final String LOCATION = "location";
+ private static final String PORT_RANGE_START = "portRangeStart";
+ private static final String PORT_RANGE_END = "portRangeEnd";
+ private static final String LOCKED = "locked";
+ private static final String IS_THREAD_SAFE = "isThreadSafe";
+ private final int responseDeadline = 20000;
+ private final int rerunRequestDelay = 200;
+ private 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);
+ /**
+ * This is either a path to the engines executable or an IP address at which the engine is running
+ */
+ private String engineLocation;
+
+ 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);
+
+ public Engine() {
+ GrpcRequestConsumer consumer = new GrpcRequestConsumer();
+ Thread consumerThread = new Thread(consumer);
+ consumerThread.start();
+ }
+
+ public Engine(final JsonObject jsonObject) {
+ this();
+ deserialize(jsonObject);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public boolean isLocal() {
+ return isLocal;
+ }
+
+ public void setLocal(boolean local) {
+ isLocal = local;
+ }
+
+ public boolean isDefault() {
+ return isDefault;
+ }
+
+ public void setDefault(boolean aDefault) {
+ isDefault = aDefault;
+ }
+
+ public boolean isThreadSafe() {
+ return isThreadSafe;
+ }
+
+ public void setIsThreadSafe(boolean threadSafe) {
+ isThreadSafe = threadSafe;
+ }
+
+ public String getEngineLocation() {
+ return engineLocation;
+ }
+
+ public void setEngineLocation(String engineLocation) {
+ this.engineLocation = engineLocation;
+ }
+
+ public String getIpAddress() {
+ if (isLocal()) {
+ return "127.0.0.1";
+ } else {
+ return getEngineLocation();
+ }
+ }
+
+ public int getPortStart() {
+ return portStart;
+ }
+
+ public void setPortStart(int portStart) {
+ this.portStart = portStart;
+ }
+
+ public int getPortEnd() {
+ return portEnd;
+ }
+
+ public void setPortEnd(int portEnd) {
+ this.portEnd = portEnd;
+ }
+
+ public int getNumberOfInstances() {
+ return this.portEnd - this.portStart + 1;
+ }
+
+ public void lockInstance() {
+ locked.set(true);
+ }
+
+ public SimpleBooleanProperty getLockedProperty() {
+ return locked;
+ }
+
+ public ArrayList getStartedConnections() {
+ return startedConnections;
+ }
+
+ /**
+ * Enqueue query for execution 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
+ */
+ public void enqueueQuery(Query query, Consumer successConsumer, Consumer errorConsumer) {
+ GrpcRequest request = new GrpcRequest(engineConnection -> {
+ var componentsInfoBuilder = BackendHelper.getComponentsInfoBuilder(query.getQuery());
+
+ 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.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);
+ });
+
+ requestQueue.add(request);
+ }
+
+ /**
+ * Signal that the EngineConnection can be used not in use and available for queries
+ *
+ * @param connection to make available
+ */
+ public 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.
+ *
+ * @return a EngineConnection object linked to the engine, either from the open engine connection list
+ * or a newly started connection.
+ * @throws BackendException.NoAvailableEngineConnectionException if unable to retrieve a connection to the engine
+ * and unable to start a new one
+ */
+ private EngineConnection getConnection() throws BackendException.NoAvailableEngineConnectionException {
+ EngineConnection connection;
+ try {
+ // If no open connection is free, attempt to start a new one
+ if (availableConnections.size() < 1) {
+ EngineConnection newConnection = this.connectionStarter.tryStartNewConnection();
+
+ if (newConnection != null) {
+ startedConnections.add(newConnection);
+ }
+ }
+
+ if (isThreadSafe){
+ connection = availableConnections.peek();
+ }
+ else{
+ // Block until a connection becomes available
+ connection = availableConnections.take();
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ return connection;
+ }
+
+ /**
+ * 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 {
+ // 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());
+
+ // Attempt to close all connections
+ for (EngineConnection ec : startedConnections) {
+ CompletableFuture closeFuture = CompletableFuture.supplyAsync(() -> {
+ try {
+ ec.close();
+ } catch (BackendException.gRpcChannelShutdownException |
+ BackendException.EngineProcessDestructionException e) {
+ throw new RuntimeException(e);
+ }
+
+ return ec;
+ });
+
+ closeFutures.add(closeFuture);
+ }
+
+ for (CompletableFuture closeFuture : closeFutures) {
+ try {
+ EngineConnection ec = closeFuture.get();
+
+ availableConnections.remove(ec);
+ startedConnections.remove(ec);
+ } catch (InterruptedException | ExecutionException e) {
+ exceptions.addSuppressed(e.getCause());
+ }
+ }
+
+ if (!startedConnections.isEmpty()) throw exceptions;
+ }
+
+ private class GrpcRequestConsumer implements Runnable {
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ GrpcRequest request = requestQueue.take();
+
+ try {
+ request.tries++;
+ request.execute(getConnection());
+ } catch (BackendException.NoAvailableEngineConnectionException e) {
+ e.printStackTrace();
+ if (request.tries < numberOfRetriesPerQuery) {
+ new Timer().schedule(new TimerTask() {
+ @Override
+ public void run() {
+ requestQueue.add(request);
+ }
+ }, rerunRequestDelay);
+ } else {
+ Ecdar.showToast("Unable to find a connection to the requested engine");
+ }
+ return;
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ @Override
+ public JsonObject serialize() {
+ final JsonObject result = new JsonObject();
+ 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());
+ result.addProperty(LOCKED, getLockedProperty().get());
+
+ return result;
+ }
+
+ @Override
+ 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());
+ if (json.getAsJsonPrimitive(LOCKED).getAsBoolean()) lockInstance();
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
diff --git a/src/main/java/ecdar/backend/EngineConnection.java b/src/main/java/ecdar/backend/EngineConnection.java
new file mode 100644
index 00000000..3cd9a5ff
--- /dev/null
+++ b/src/main/java/ecdar/backend/EngineConnection.java
@@ -0,0 +1,84 @@
+package ecdar.backend;
+
+import EcdarProtoBuf.EcdarBackendGrpc;
+import io.grpc.ManagedChannel;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class EngineConnection {
+ private final Engine engine;
+ private final EcdarBackendGrpc.EcdarBackendStub stub;
+ private final ManagedChannel channel;
+ private final Process process;
+ private final int port;
+
+ EngineConnection(Engine engine, ManagedChannel channel, EcdarBackendGrpc.EcdarBackendStub stub, Process process) {
+ this.engine = engine;
+ this.stub = stub;
+ this.channel = channel;
+ this.process = process;
+ this.port = Integer.parseInt(getStub().getChannel().authority().split(":", 2)[1]);
+ }
+
+ EngineConnection(Engine engine, ManagedChannel channel, EcdarBackendGrpc.EcdarBackendStub stub) {
+ this(engine, channel, stub, null);
+ }
+
+ /**
+ * Get the gRPC stub of the connection to use for query execution
+ *
+ * @return the gRPC stub of this connection
+ */
+ public EcdarBackendGrpc.EcdarBackendStub getStub() {
+ return stub;
+ }
+
+ /**
+ * Get the engine that should be used to execute
+ * the query currently associated with this engine connection
+ *
+ * @return the instance of the associated executable query object,
+ * or null, if no executable query is currently associated
+ */
+ public Engine getEngine() {
+ return engine;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ * Close the gRPC connection and end the process
+ *
+ * @throws BackendException.gRpcChannelShutdownException if an InterruptedException is encountered while trying to shut down the gRPC channel.
+ * @throws BackendException.EngineProcessDestructionException if the connected engine process throws an ExecutionException, an InterruptedException, or a TimeoutException.
+ */
+ public void close() throws BackendException.gRpcChannelShutdownException, BackendException.EngineProcessDestructionException {
+ if (!channel.isShutdown()) {
+ try {
+ channel.shutdown();
+ if (!channel.awaitTermination(45, TimeUnit.SECONDS)) {
+ channel.shutdownNow(); // Forcefully close the connection
+ }
+ } catch (InterruptedException e) {
+ // Engine location is either the file path or the IP, here we want the channel address
+ throw new BackendException.gRpcChannelShutdownException("The gRPC channel to \"" + this.engine.getName() + "\" instance running at: " + engine.getIpAddress() + ":" + this.port + "was interrupted during termination", e.getCause());
+ }
+ }
+
+ // If the engine is remote, there will not be a process
+ if (process != null) {
+ try {
+ java.util.concurrent.CompletableFuture terminated = process.onExit();
+ process.destroy();
+ terminated.get(45, TimeUnit.SECONDS);
+ } catch (ExecutionException | InterruptedException | TimeoutException e) {
+ // Add the engine location to the exception, as it contains the path to the executable
+ throw new BackendException.EngineProcessDestructionException("A process running: " + this.engine.getEngineLocation() + " on port " + this.port + " threw an exception during shutdown", e.getCause());
+ }
+ }
+ }
+}
diff --git a/src/main/java/ecdar/backend/EngineConnectionStarter.java b/src/main/java/ecdar/backend/EngineConnectionStarter.java
new file mode 100644
index 00000000..be8c3a2f
--- /dev/null
+++ b/src/main/java/ecdar/backend/EngineConnectionStarter.java
@@ -0,0 +1,119 @@
+package ecdar.backend;
+
+import EcdarProtoBuf.EcdarBackendGrpc;
+import ecdar.Ecdar;
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+import org.springframework.util.SocketUtils;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+public class EngineConnectionStarter {
+ private final Engine engine;
+ private final int maxRetriesForStartingEngineProcess = 3;
+
+ EngineConnectionStarter(Engine engine) {
+ this.engine = engine;
+ }
+
+ /**
+ * Attempts to start a new connection to the specified engine.
+ *
+ * @return the started EngineConnection if successful,
+ * otherwise, null.
+ */
+ protected EngineConnection tryStartNewConnection() {
+ EngineConnection newConnection;
+
+ if (engine.isLocal()) {
+ newConnection = startLocalConnection();
+ } else {
+ newConnection = startRemoteConnection();
+ }
+
+ // If the connection is null, no new connection was started
+ return newConnection;
+ }
+
+ /**
+ * Starts a process, creates an EngineConnection to it, and returns that connection
+ *
+ * @return an EngineConnection to a local engine running in a Process or null if all ports are already in use
+ */
+ private EngineConnection startLocalConnection() {
+ long port;
+ try {
+ port = SocketUtils.findAvailableTcpPort(engine.getPortStart(), engine.getPortEnd());
+ } catch (IllegalStateException e) {
+ // All ports specified for engine are already used for running engines
+ return null;
+ }
+
+ // Start local process of engine
+ Process p;
+ int attempts = 0;
+
+ do {
+ attempts++;
+ ProcessBuilder pb = new ProcessBuilder(engine.getEngineLocation(), "-p", engine.getIpAddress() + ":" + port);
+
+ try {
+ p = pb.start();
+ } catch (IOException ioException) {
+ Ecdar.showToast("Unable to start local engine instance");
+ ioException.printStackTrace();
+ return null;
+ }
+ } while (!p.isAlive() && attempts < maxRetriesForStartingEngineProcess);
+
+ ManagedChannel channel = startGrpcChannel(engine.getIpAddress(), port);
+ EcdarBackendGrpc.EcdarBackendStub stub = EcdarBackendGrpc.newStub(channel);
+ return new EngineConnection(engine, channel, stub, p);
+ }
+
+ /**
+ * Creates and returns an EngineConnection to the remote engine
+ *
+ * @return an EngineConnection to a remote engine or null if all ports are already connected to
+ */
+ private EngineConnection startRemoteConnection() {
+ // Get a stream of ports already used for connections
+ Supplier> activeEnginePortsStream = () -> engine.getStartedConnections().stream()
+ .mapToInt(EngineConnection::getPort).boxed();
+
+ long port = engine.getPortStart();
+ for (int currentPort = engine.getPortStart(); currentPort <= engine.getPortEnd(); currentPort++) {
+ final int tempPort = currentPort;
+ if (activeEnginePortsStream.get().anyMatch((i) -> i == tempPort)) {
+ port = currentPort;
+ break;
+ }
+ }
+
+ if (port > engine.getPortEnd()) {
+ // All ports specified for engine are already used for connections
+ return null;
+ }
+
+ ManagedChannel channel = startGrpcChannel(engine.getIpAddress(), port);
+ EcdarBackendGrpc.EcdarBackendStub stub = EcdarBackendGrpc.newStub(channel);
+ return new EngineConnection(engine, channel, stub);
+ }
+
+ /**
+ * Connects a gRPC channel to the address at the specified port, expecting that an engine is running there
+ *
+ * @param address of the target engine
+ * @param port of the target engine at the address
+ * @return the created gRPC channel
+ */
+ private ManagedChannel startGrpcChannel(final String address, final long port) {
+ return ManagedChannelBuilder.forTarget(address + ":" + port)
+ .usePlaintext()
+ .keepAliveTime(1000, TimeUnit.MILLISECONDS)
+ .build();
+ }
+}
diff --git a/src/main/java/ecdar/backend/GrpcRequest.java b/src/main/java/ecdar/backend/GrpcRequest.java
index d6c667f8..642ebfd0 100644
--- a/src/main/java/ecdar/backend/GrpcRequest.java
+++ b/src/main/java/ecdar/backend/GrpcRequest.java
@@ -3,20 +3,14 @@
import java.util.function.Consumer;
public class GrpcRequest {
- private final Consumer request;
- private final BackendInstance backend;
+ private final Consumer request;
public int tries = 0;
- public GrpcRequest(Consumer request, BackendInstance backend) {
+ public GrpcRequest(Consumer request) {
this.request = request;
- this.backend = backend;
}
- public void execute(BackendConnection backendConnection) {
- this.request.accept(backendConnection);
- }
-
- public BackendInstance getBackend() {
- return backend;
+ public void execute(EngineConnection engineConnection) {
+ this.request.accept(engineConnection);
}
}
\ No newline at end of file
diff --git a/src/main/java/ecdar/backend/QueryHandler.java b/src/main/java/ecdar/backend/QueryHandler.java
deleted file mode 100644
index 40142d64..00000000
--- a/src/main/java/ecdar/backend/QueryHandler.java
+++ /dev/null
@@ -1,272 +0,0 @@
-package ecdar.backend;
-
-import EcdarProtoBuf.QueryProtos;
-import EcdarProtoBuf.QueryProtos.QueryRequest.Settings;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import ecdar.Ecdar;
-import ecdar.abstractions.Component;
-import ecdar.abstractions.Query;
-import ecdar.abstractions.QueryState;
-import ecdar.abstractions.QueryType;
-import ecdar.controllers.EcdarController;
-import ecdar.utility.UndoRedoStack;
-import ecdar.utility.helpers.StringValidator;
-import io.grpc.stub.StreamObserver;
-import javafx.application.Platform;
-import javafx.collections.ObservableList;
-
-import java.util.ArrayList;
-import java.util.NoSuchElementException;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-
-public class QueryHandler {
- private final BackendDriver backendDriver;
- private final ArrayList connections = new ArrayList<>();
-
- public QueryHandler(BackendDriver backendDriver) {
- this.backendDriver = backendDriver;
- }
-
- /**
- * Executes the specified query
- * @param query query to be executed
- */
- public void executeQuery(Query query) throws NoSuchElementException {
- if (query.getQueryState().equals(QueryState.RUNNING) || !StringValidator.validateQuery(query.getQuery())) return;
-
- if (query.getQuery().isEmpty()) {
- query.setQueryState(QueryState.SYNTAX_ERROR);
- query.addError("Query is empty");
- return;
- }
-
- query.setQueryState(QueryState.RUNNING);
- query.errors().set("");
-
- GrpcRequest request = new GrpcRequest(backendConnection -> {
- connections.add(backendConnection); // Save reference for closing connection on exit
-
- var componentsInfoBuilder = BackendHelper.getComponentsInfoBuilder(query.getQuery());
-
- StreamObserver responseObserver = new StreamObserver<>() {
- @Override
- public void onNext(QueryProtos.QueryResponse value) {
- handleQueryResponse(value, query);
- }
-
- @Override
- public void onError(Throwable t) {
- handleQueryBackendError(t, query);
-
- // Release backend connection
- backendDriver.addBackendConnection(backendConnection);
- connections.remove(backendConnection);
- }
-
- @Override
- public void onCompleted() {
- // Release backend connection
- backendDriver.addBackendConnection(backendConnection);
- connections.remove(backendConnection);
- }
- };
-
- var queryBuilder = QueryProtos.QueryRequest.newBuilder()
- .setUserId(1)
- .setQueryId(UUID.randomUUID().hashCode())
- .setSettings(Settings.newBuilder().setDisableClockReduction(true))
- .setQuery(query.getType().getQueryName() + ": " + query.getQuery())
- .setComponentsInfo(componentsInfoBuilder);
-
- backendConnection.getStub().withDeadlineAfter(backendDriver.getResponseDeadline(), TimeUnit.MILLISECONDS)
- .sendQuery(queryBuilder.build(), responseObserver);
- }, query.getBackend());
-
- backendDriver.addRequestToExecutionQueue(request);
- }
-
- /**
- * Close all open backend connection and kill all locally running processes
- */
- public void closeAllBackendConnections() {
- for (BackendConnection con : connections) {
- con.close();
- }
- }
-
- private void handleQueryResponse(QueryProtos.QueryResponse value, Query query) {
- // If the query has been cancelled, ignore the result
- if (query.getQueryState() == QueryState.UNKNOWN) return;
- switch (value.getResultCase()) {
- case REFINEMENT:
- if (value.getRefinement().getSuccess()) {
- query.setQueryState(QueryState.SUCCESSFUL);
- query.getSuccessConsumer().accept(true);
- } else {
- query.setQueryState(QueryState.ERROR);
- query.getFailureConsumer().accept(new BackendException.QueryErrorException(value.getRefinement().getReason()));
- query.getSuccessConsumer().accept(false);
- query.getStateActionConsumer().accept(value.getRefinement().getState(),
- value.getRefinement().getActionList());
- }
- break;
-
- case CONSISTENCY:
- if (value.getConsistency().getSuccess()) {
- query.setQueryState(QueryState.SUCCESSFUL);
- query.getSuccessConsumer().accept(true);
- } else {
- query.setQueryState(QueryState.ERROR);
- query.getFailureConsumer().accept(new BackendException.QueryErrorException(value.getConsistency().getReason()));
- query.getSuccessConsumer().accept(false);
- query.getStateActionConsumer().accept(value.getConsistency().getState(),
- value.getConsistency().getActionList());
- }
- break;
-
- case DETERMINISM:
- if (value.getDeterminism().getSuccess()) {
- query.setQueryState(QueryState.SUCCESSFUL);
- query.getSuccessConsumer().accept(true);
- } else {
- query.setQueryState(QueryState.ERROR);
- query.getFailureConsumer().accept(new BackendException.QueryErrorException(value.getDeterminism().getReason()));
- query.getSuccessConsumer().accept(false);
- query.getStateActionConsumer().accept(value.getDeterminism().getState(),
- value.getDeterminism().getActionList());
-
- }
- break;
-
- case IMPLEMENTATION:
- if (value.getImplementation().getSuccess()) {
- query.setQueryState(QueryState.SUCCESSFUL);
- query.getSuccessConsumer().accept(true);
- } else {
- query.setQueryState(QueryState.ERROR);
- query.getFailureConsumer().accept(new BackendException.QueryErrorException(value.getImplementation().getReason()));
- query.getSuccessConsumer().accept(false);
- //ToDo: These errors are not implemented in the Reveaal backend.
- query.getStateActionConsumer().accept(value.getImplementation().getState(),
- new ArrayList<>());
- }
- break;
-
- case REACHABILITY:
- if (value.getReachability().getSuccess()) {
- query.setQueryState(QueryState.SUCCESSFUL);
- Ecdar.showToast("Reachability check was successful and the location can be reached.");
-
- //create list of edge id's
- ArrayList edgeIds = new ArrayList<>();
- for(var pathsList : value.getReachability().getComponentPathsList()){
- for(var id : pathsList.getEdgeIdsList().toArray()) {
- edgeIds.add(id.toString());
- }
- }
- //highlight the edges
- Ecdar.getSimulationHandler().highlightReachabilityEdges(edgeIds);
- query.getSuccessConsumer().accept(true);
- }
- else if(!value.getReachability().getSuccess()){
- Ecdar.showToast("Reachability check was successful but the location cannot be reached.");
- query.getSuccessConsumer().accept(true);
- } else {
- query.setQueryState(QueryState.ERROR);
- Ecdar.showToast("Error from backend: Reachability check was unsuccessful!");
- query.getFailureConsumer().accept(new BackendException.QueryErrorException(value.getReachability().getReason()));
- query.getSuccessConsumer().accept(false);
- //ToDo: These errors are not implemented in the Reveaal backend.
- query.getStateActionConsumer().accept(value.getReachability().getState(),
- new ArrayList<>());
- }
- break;
-
- case COMPONENT:
- query.setQueryState(QueryState.SUCCESSFUL);
- query.getSuccessConsumer().accept(true);
- JsonObject returnedComponent = (JsonObject) JsonParser.parseString(value.getComponent().getComponent().getJson());
- addGeneratedComponent(new Component(returnedComponent));
- break;
-
- case ERROR:
- query.setQueryState(QueryState.ERROR);
- Ecdar.showToast(value.getError());
- query.getFailureConsumer().accept(new BackendException.QueryErrorException(value.getError()));
- query.getSuccessConsumer().accept(false);
- break;
-
- case RESULT_NOT_SET:
- query.setQueryState(QueryState.ERROR);
- query.getSuccessConsumer().accept(false);
- break;
- }
- }
-
- private void handleQueryBackendError(Throwable t, Query query) {
- // If the query has been cancelled, ignore the error
- if (query.getQueryState() == QueryState.UNKNOWN) return;
-
- // due to lack of information from backend if the reachability check shows that a location can NOT be reached, this is the most accurate information we can provide
- if(query.getType() == QueryType.REACHABILITY){
- Ecdar.showToast("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];
-
- if ("DEADLINE_EXCEEDED".equals(errorType)) {
- query.setQueryState(QueryState.ERROR);
- query.getFailureConsumer().accept(new BackendException.QueryErrorException("The backend did not answer the request in time"));
- } else {
- try {
- query.setQueryState(QueryState.ERROR);
- query.getFailureConsumer().accept(new BackendException.QueryErrorException("The execution of this query failed with message:" + System.lineSeparator() + t.getLocalizedMessage()));
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
-
- private void addGeneratedComponent(Component newComponent) {
- Platform.runLater(() -> {
- newComponent.setTemporary(true);
-
- ObservableList listOfGeneratedComponents = Ecdar.getProject().getTempComponents(); // ToDo NIELS: Refactor
- Component matchedComponent = null;
-
- for (Component currentGeneratedComponent : listOfGeneratedComponents) {
- int comparisonOfNames = currentGeneratedComponent.getName().compareTo(newComponent.getName());
-
- if (comparisonOfNames == 0) {
- matchedComponent = currentGeneratedComponent;
- break;
- } else if (comparisonOfNames < 0) {
- break;
- }
- }
-
- if (matchedComponent == null) {
- UndoRedoStack.pushAndPerform(() -> { // Perform
- Ecdar.getProject().getTempComponents().add(newComponent);
- }, () -> { // Undo
- Ecdar.getProject().getTempComponents().remove(newComponent);
- }, "Created new component: " + newComponent.getName(), "add-circle");
- } else {
- // Remove current component with name and add the newly generated one
- Component finalMatchedComponent = matchedComponent;
- UndoRedoStack.pushAndPerform(() -> { // Perform
- Ecdar.getProject().getTempComponents().remove(finalMatchedComponent);
- Ecdar.getProject().getTempComponents().add(newComponent);
- }, () -> { // Undo
- Ecdar.getProject().getTempComponents().remove(newComponent);
- Ecdar.getProject().getTempComponents().add(finalMatchedComponent);
- }, "Created new component: " + newComponent.getName(), "add-circle");
- }
-
- EcdarController.setActiveModelForActiveCanvas(newComponent);
- });
- }
-}
diff --git a/src/main/java/ecdar/backend/SimulationHandler.java b/src/main/java/ecdar/backend/SimulationHandler.java
index 4fa74b03..1dab3e5d 100644
--- a/src/main/java/ecdar/backend/SimulationHandler.java
+++ b/src/main/java/ecdar/backend/SimulationHandler.java
@@ -16,7 +16,6 @@
import javafx.collections.ObservableMap;
import javafx.util.Pair;
-import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@@ -45,16 +44,16 @@ public class SimulationHandler {
private final ObservableMap simulationVariables = FXCollections.observableHashMap();
private final ObservableMap simulationClocks = FXCollections.observableHashMap();
public ObservableList traceLog = FXCollections.observableArrayList();
- private final BackendDriver backendDriver;
- private final ArrayList connections = new ArrayList<>();
private List ComponentsInSimulation = new ArrayList<>();
+ private EngineConnection con;
+
/**
* Empty constructor that should be used if the system or project has not be initialized yet
*/
- public SimulationHandler(BackendDriver backendDriver) {
- this.backendDriver = backendDriver;
+ public SimulationHandler() {
+
}
public void clearComponentsInSimulation() {
@@ -75,6 +74,11 @@ private void initializeSimulation() {
this.selectedEdge.set(null);
this.traceLog.clear();
this.system = getSystem();
+
+ if (con == null) {
+ EngineConnectionStarter ecs = new EngineConnectionStarter(BackendHelper.getDefaultEngine());
+ con = ecs.tryStartNewConnection();
+ }
}
/**
@@ -83,11 +87,11 @@ private void initializeSimulation() {
public void initialStep() {
initializeSimulation();
- GrpcRequest request = new GrpcRequest(backendConnection -> {
+ GrpcRequest request = new GrpcRequest(engineConnection -> {
StreamObserver responseObserver = new StreamObserver<>() {
@Override
public void onNext(QueryProtos.SimulationStepResponse value) {
- // TODO this is temp solution to compile but should be fixed to handle ambiguity
+ // ToDo: This is temp solution to compile but should be fixed to handle ambiguity
currentState.set(new SimulationState(value.getNewDecisionPoints(0)));
Platform.runLater(() -> traceLog.add(currentState.get()));
}
@@ -95,17 +99,10 @@ public void onNext(QueryProtos.SimulationStepResponse value) {
@Override
public void onError(Throwable t) {
Ecdar.showToast("Could not start simulation:\n" + t.getMessage());
-
- // Release backend connection
- backendDriver.addBackendConnection(backendConnection);
- connections.remove(backendConnection);
}
@Override
public void onCompleted() {
- // Release backend connection
- backendDriver.addBackendConnection(backendConnection);
- connections.remove(backendConnection);
}
};
@@ -120,12 +117,11 @@ public void onCompleted() {
.setComponentComposition(composition)
.setComponentsInfo(comInfo);
simStartRequest.setSimulationInfo(simInfo);
- backendConnection.getStub().withDeadlineAfter(this.backendDriver.getResponseDeadline(), TimeUnit.MILLISECONDS)
+ engineConnection.getStub().withDeadlineAfter(20000, TimeUnit.MILLISECONDS)
.startSimulation(simStartRequest.build(), responseObserver);
- }, BackendHelper.getDefaultBackendInstance());
-
- backendDriver.addRequestToExecutionQueue(request);
-
+ });
+
+ request.execute(con);
numberOfSteps++;
}
@@ -143,7 +139,7 @@ public void nextStep() {
// removes invalid states from the log when stepping forward after previewing a previous state
removeStatesFromLog(currentState.get());
- GrpcRequest request = new GrpcRequest(backendConnection -> {
+ GrpcRequest request = new GrpcRequest(engineConnection -> {
StreamObserver responseObserver = new StreamObserver<>() {
@Override
public void onNext(QueryProtos.SimulationStepResponse value) {
@@ -155,17 +151,10 @@ public void onNext(QueryProtos.SimulationStepResponse value) {
@Override
public void onError(Throwable t) {
Ecdar.showToast("Could not take next step in simulation\nError: " + t.getMessage());
-
- // Release backend connection
- backendDriver.addBackendConnection(backendConnection);
- connections.remove(backendConnection);
- }
+ }
@Override
public void onCompleted() {
- // Release backend connection
- backendDriver.addBackendConnection(backendConnection);
- connections.remove(backendConnection);
}
};
@@ -173,6 +162,7 @@ public void onCompleted() {
for (Component c : Ecdar.getProject().getComponents()) {
comInfo.addComponents(ComponentProtos.Component.newBuilder().setJson(c.serialize().toString()).build());
}
+
comInfo.setComponentsHash(comInfo.getComponentsList().hashCode());
var simStepRequest = SimulationStepRequest.newBuilder();
var simInfo = SimulationInfo.newBuilder()
@@ -185,13 +175,11 @@ public void onCompleted() {
var decision = Decision.newBuilder().setEdge(edge).setSource(source);
simStepRequest.setChosenDecision(decision);
- backendConnection.getStub().withDeadlineAfter(this.backendDriver.getResponseDeadline(), TimeUnit.MILLISECONDS)
+ engineConnection.getStub().withDeadlineAfter(20000, TimeUnit.MILLISECONDS)
.takeSimulationStep(simStepRequest.build(), responseObserver);
- }, BackendHelper.getDefaultBackendInstance());
-
- backendDriver.addRequestToExecutionQueue(request);
+ });
- // increments the number of steps taken during this simulation
+ request.execute(con);
numberOfSteps++;
}
@@ -297,17 +285,6 @@ public boolean isSimulationRunning() {
return false; // ToDo: Implement
}
- /**
- * Close all open backend connection and kill all locally running processes
- *
- * @throws IOException if any of the sockets do not respond
- */
- public void closeAllBackendConnections() throws IOException {
- for (BackendConnection con : connections) {
- con.close();
- }
- }
-
/**
* Removes all states from the trace log after the given state
*/
diff --git a/src/main/java/ecdar/code_analysis/CodeAnalysis.java b/src/main/java/ecdar/code_analysis/CodeAnalysis.java
index d5f9bd74..c3599ceb 100644
--- a/src/main/java/ecdar/code_analysis/CodeAnalysis.java
+++ b/src/main/java/ecdar/code_analysis/CodeAnalysis.java
@@ -188,5 +188,4 @@ public static void enable() {
public static void disable() {
ENABLED = false;
}
-
}
diff --git a/src/main/java/ecdar/code_analysis/Nearable.java b/src/main/java/ecdar/code_analysis/Nearable.java
index fdad43cb..94d3a5a6 100644
--- a/src/main/java/ecdar/code_analysis/Nearable.java
+++ b/src/main/java/ecdar/code_analysis/Nearable.java
@@ -1,7 +1,5 @@
package ecdar.code_analysis;
public interface Nearable {
-
String generateNearString();
-
}
diff --git a/src/main/java/ecdar/controllers/BackendOptionsDialogController.java b/src/main/java/ecdar/controllers/BackendOptionsDialogController.java
deleted file mode 100644
index 1950f30b..00000000
--- a/src/main/java/ecdar/controllers/BackendOptionsDialogController.java
+++ /dev/null
@@ -1,496 +0,0 @@
-package ecdar.controllers;
-
-import com.google.gson.JsonArray;
-import com.google.gson.JsonParser;
-import com.jfoenix.controls.JFXButton;
-import com.jfoenix.controls.JFXRippler;
-import ecdar.Ecdar;
-import ecdar.backend.BackendInstance;
-import ecdar.backend.BackendHelper;
-import ecdar.presentations.BackendInstancePresentation;
-import javafx.fxml.Initializable;
-import javafx.scene.Node;
-import javafx.scene.control.ToggleGroup;
-import javafx.scene.layout.HBox;
-import javafx.scene.layout.Priority;
-import javafx.scene.layout.VBox;
-import org.apache.commons.lang3.Range;
-import org.apache.commons.lang3.SystemUtils;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.net.UnknownHostException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.*;
-
-public class BackendOptionsDialogController implements Initializable {
- public VBox backendInstanceList;
- public JFXRippler addBackendButton;
- public JFXButton closeButton;
- public ToggleGroup defaultBackendToggleGroup = new ToggleGroup();
- public JFXButton saveButton;
- public JFXButton resetBackendsButton;
-
- @Override
- public void initialize(URL location, ResourceBundle resources) {
- initializeBackendInstanceList();
- }
-
- /**
- * Reverts any changes made to the backend options by reloading the options specified in the preference file,
- * or to the default, if no backends are present in the preferences file.
- */
- public void cancelBackendOptionsChanges() {
- initializeBackendInstanceList();
- }
-
- /**
- * Saves the changes made to the backend options to the preferences file and returns true
- * if no errors where found in the backend instance definitions, otherwise false.
- *
- * @return whether the changes could be saved,
- * meaning that no errors where found in the changes made to the backend options
- */
- public boolean saveChangesToBackendOptions() {
- if (this.backendInstanceListIsErrorFree()) {
- ArrayList backendInstances = new ArrayList<>();
- for (Node backendInstance : backendInstanceList.getChildren()) {
- if (backendInstance instanceof BackendInstancePresentation) {
- backendInstances.add(((BackendInstancePresentation) backendInstance).getController().updateBackendInstance());
- }
- }
-
- if (backendInstances.size() < 1) {
- Ecdar.showToast("Please add an engine instance or press: \"" + resetBackendsButton.getText() + "\"");
- return false;
- }
-
- // Close all backend connections to avoid dangling backend connections when port range is changed
- try {
- Ecdar.getBackendDriver().closeAllBackendConnections();
- Ecdar.getQueryExecutor().closeAllBackendConnections();
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- BackendHelper.updateBackendInstances(backendInstances);
-
- JsonArray jsonArray = new JsonArray();
- for (BackendInstance bi : backendInstances) {
- jsonArray.add(bi.serialize());
- }
-
- Ecdar.preferences.put("backend_instances", jsonArray.toString());
-
- BackendInstance defaultBackend = backendInstances.stream().filter(BackendInstance::isDefault).findFirst().orElse(backendInstances.get(0));
- BackendHelper.setDefaultBackendInstance(defaultBackend);
-
- String defaultBackendName = (defaultBackend.getName());
- Ecdar.preferences.put("default_backend", defaultBackendName);
-
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Resets the backends to the default backends present in the 'default_backends.json' file.
- */
- public void resetBackendsToDefault() {
- updateBackendsInGUI(getPackagedBackends());
- }
-
- private void initializeBackendInstanceList() {
- ArrayList backends;
-
- // Load backends from preferences or get default
- var savedBackends = Ecdar.preferences.get("backend_instances", null);
- if (savedBackends != null) {
- backends = getBackendsFromJsonArray(
- JsonParser.parseString(savedBackends).getAsJsonArray());
- } else {
- backends = getPackagedBackends();
- }
-
- // Style add backend button and handle click event
- HBox.setHgrow(addBackendButton, Priority.ALWAYS);
- addBackendButton.setMaxWidth(Double.MAX_VALUE);
- addBackendButton.setOnMouseClicked((event) -> {
- BackendInstancePresentation newBackendInstancePresentation = new BackendInstancePresentation();
- addBackendInstancePresentationToList(newBackendInstancePresentation);
- });
-
- updateBackendsInGUI(backends);
- }
-
- /**
- * Clear the backend instance list and add the newly defined backends to it.
- *
- * @param backends The new list of backends
- */
- private void updateBackendsInGUI(ArrayList backends) {
- backendInstanceList.getChildren().clear();
-
- backends.forEach((bi) -> {
- BackendInstancePresentation newBackendInstancePresentation = new BackendInstancePresentation(bi);
-
- // Bind input fields that should not be changed for packaged backends to the locked property of the backend instance
- newBackendInstancePresentation.getController().backendName.disableProperty().bind(bi.getLockedProperty());
- newBackendInstancePresentation.getController().pathToBackend.disableProperty().bind(bi.getLockedProperty());
- newBackendInstancePresentation.getController().pickPathToBackend.disableProperty().bind(bi.getLockedProperty());
- newBackendInstancePresentation.getController().isLocal.disableProperty().bind(bi.getLockedProperty());
- addBackendInstancePresentationToList(newBackendInstancePresentation);
- });
-
- BackendHelper.updateBackendInstances(backends);
- }
-
- /**
- * Instantiate backends defined in the given JsonArray.
- *
- * @param backends The JsonArray containing the backends
- * @return An ArrayList of the instantiated backends
- */
- private ArrayList getBackendsFromJsonArray(JsonArray backends) {
- ArrayList backendInstances = new ArrayList<>();
- backendInstanceList.getChildren().clear();
- backends.forEach((backend) -> {
- BackendInstance newBackendInstance = new BackendInstance(backend.getAsJsonObject());
- backendInstances.add(newBackendInstance);
- });
-
- return backendInstances;
- }
-
- /**
- * Checks a set of paths to the packaged engines, j-Ecdar and Reveaal, and instantiates them
- * if one of the related files exists.
- *
- * @return Backend instances of the packaged engines
- */
- private ArrayList getPackagedBackends() {
- ArrayList defaultBackends = new ArrayList<>();
-
- // Add Reveaal engine
- var reveaal = new BackendInstance();
- reveaal.setName("Reveaal");
- reveaal.setLocal(true);
- reveaal.setDefault(true);
-
- // The engine is thread-safe, a range just adds options for finding an open port
- // Only one process will be started
- reveaal.setPortStart(5040);
- reveaal.setPortEnd(5042);
- reveaal.lockInstance();
- reveaal.setIsThreadSafe(true);
-
- // Load correct Reveaal executable based on OS
- List potentialFilesForReveaal = new ArrayList<>();
- if (SystemUtils.IS_OS_WINDOWS) {
- potentialFilesForReveaal.add("Reveaal.exe");
- } else {
- potentialFilesForReveaal.add("Reveaal");
- }
- if (setBackendPathIfFileExists(reveaal, potentialFilesForReveaal)) defaultBackends.add(reveaal);
-
- // Add jECDAR engine
- var jEcdar = new BackendInstance();
- jEcdar.setName("j-Ecdar");
- jEcdar.setLocal(true);
- jEcdar.setDefault(false);
- jEcdar.setPortStart(5042);
- jEcdar.setPortEnd(5050);
- jEcdar.lockInstance();
- jEcdar.setIsThreadSafe(false);
-
- // Load correct j-Ecdar executable based on OS
- List potentialFiledForJEcdar = new ArrayList<>();
- if (SystemUtils.IS_OS_WINDOWS) {
- potentialFiledForJEcdar.add("j-Ecdar.bat");
- } else {
- potentialFiledForJEcdar.add("j-Ecdar");
- }
-
- if (setBackendPathIfFileExists(jEcdar, potentialFiledForJEcdar)) defaultBackends.add(jEcdar);
-
- return defaultBackends;
- }
-
- /**
- * Sets the path to the backend instance if one of the potential files exists
- *
- * @param engine The backend instance of the engine to set the path for
- * @param potentialFiles List of potential files to use for the engine
- * @return True if one of the potentialFiles where found in path, false otherwise.
- * This value also signals whether the engine backendLocation is set
- */
- private boolean setBackendPathIfFileExists(BackendInstance engine, List potentialFiles) {
- engine.setBackendLocation("");
-
- try {
- // Get directory containing the bin and lib folders for the executing program
- String pathToEcdarDirectory = new File(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParentFile().getPath();
-
- List files = List.of(Objects.requireNonNull(new File(pathToEcdarDirectory).listFiles()));
- for (File f : files) {
- if (potentialFiles.contains(f.getName())) {
- engine.setBackendLocation(f.getAbsolutePath());
- return true;
- }
- }
- } catch (URISyntaxException e) {
- e.printStackTrace();
- Ecdar.showToast("Unable to get URI of parent directory: \"" + getClass().getProtectionDomain().getCodeSource().getLocation() + "\" due to: " + e.getMessage());
- } catch (NullPointerException e) {
- e.printStackTrace();
- Ecdar.showToast("Encountered null reference when trying to get path of executing program");
- }
-
- return !engine.getBackendLocation().equals("");
- }
-
- /**
- * Add the new backend instance presentation to the backend options dialog
- * @param newBackendInstancePresentation The presentation of the new backend instance
- */
- private void addBackendInstancePresentationToList(BackendInstancePresentation newBackendInstancePresentation) {
- backendInstanceList.getChildren().add(newBackendInstancePresentation);
- newBackendInstancePresentation.getController().moveBackendInstanceUpRippler.setOnMouseClicked((mouseEvent) -> moveBackendInstance(newBackendInstancePresentation, -1));
- newBackendInstancePresentation.getController().moveBackendInstanceDownRippler.setOnMouseClicked((mouseEvent) -> moveBackendInstance(newBackendInstancePresentation, +1));
-
- // Set remove backend action to only fire if the backend is not locked
- newBackendInstancePresentation.getController().removeBackendRippler.setOnMouseClicked((mouseEvent) -> {
- if (!newBackendInstancePresentation.getController().defaultBackendRadioButton.isSelected()) {
- backendInstanceList.getChildren().remove(newBackendInstancePresentation);
- }
- });
- newBackendInstancePresentation.getController().defaultBackendRadioButton.setToggleGroup(defaultBackendToggleGroup);
- }
-
- /**
- * Calculated the location new position of the backend instance, i places further down, in the backend instance list.
- * The backend instance presentation is removed at added to the new position.
- * Given a negative value, the instance is moved up. This function uses loop-around, meaning that:
- * - If the instance is moved down while already at the bottom of the list, it is placed at the top.
- * - If the instance is moved up while already at the top of the list, it is placed at the bottom.
- *
- * @param backendInstancePresentation The backend instance presentation to move
- * @param i The number of steps to move the backend instance down
- */
- private void moveBackendInstance(BackendInstancePresentation backendInstancePresentation, int i) {
- int currentIndex = backendInstanceList.getChildren().indexOf(backendInstancePresentation);
- int newIndex = (currentIndex + i) % backendInstanceList.getChildren().size();
- if (newIndex < 0) {
- newIndex = backendInstanceList.getChildren().size() - 1;
- }
-
- backendInstanceList.getChildren().remove(backendInstancePresentation);
- backendInstanceList.getChildren().add(newIndex, backendInstancePresentation);
- }
-
- /**
- * Marks input fields in the backendInstanceList that contains errors and returns whether any errors were found
- *
- * @return whether any errors were found
- */
- private boolean backendInstanceListIsErrorFree() {
- boolean error = true;
-
- for (Node child : backendInstanceList.getChildren()) {
- if (child instanceof BackendInstancePresentation) {
- BackendInstanceController backendInstanceController = ((BackendInstancePresentation) child).getController();
- error = backendNameIsErrorFree(backendInstanceController) && error;
- error = portRangeIsErrorFree(backendInstanceController) && error;
- error = backendInstanceLocationIsErrorFree(backendInstanceController) && error;
- }
- }
-
- return error;
- }
-
- private boolean backendNameIsErrorFree(BackendInstanceController backendInstanceController) {
- String backendName = backendInstanceController.backendName.getText();
-
- if (backendName.isBlank()) {
- backendInstanceController.backendNameIssue.setText(ValidationErrorMessages.BACKEND_NAME_EMPTY.toString());
- backendInstanceController.backendNameIssue.setVisible(true);
- return false;
- }
-
- backendInstanceController.backendNameIssue.setVisible(false);
- return true;
- }
-
- private boolean portRangeIsErrorFree(BackendInstanceController backendInstanceController) {
- boolean errorFree = true;
- int portRangeStart = 0, portRangeEnd = 0;
- backendInstanceController.portRangeStartIssue.setText("");
- backendInstanceController.portRangeStartIssue.setVisible(false);
- backendInstanceController.portRangeEndIssue.setText("");
- backendInstanceController.portRangeEndIssue.setVisible(false);
- backendInstanceController.portRangeIssue.setVisible(false);
-
- try {
- portRangeStart = Integer.parseInt(backendInstanceController.portRangeStart.getText());
- } catch (NumberFormatException numberFormatException) {
- backendInstanceController.portRangeStartIssue.setText(ValidationErrorMessages.VALUE_NOT_INTEGER.toString());
- errorFree = false;
- }
-
- try {
- portRangeEnd = Integer.parseInt(backendInstanceController.portRangeEnd.getText());
- } catch (NumberFormatException numberFormatException) {
- backendInstanceController.portRangeEndIssue.setText(ValidationErrorMessages.VALUE_NOT_INTEGER.toString());
- errorFree = false;
- }
-
- Range portRange = Range.between(0, 65535);
-
- if (!portRange.contains(portRangeStart)) {
- if (backendInstanceController.portRangeStartIssue.getText().isBlank()) {
- backendInstanceController.portRangeStartIssue.setText(ValidationErrorMessages.PORT_VALUE_NOT_WITHIN_ACCEPTABLE_RANGE.toString());
- } else {
- backendInstanceController.portRangeStartIssue.setText(ValidationErrorMessages.PORT_VALUE_NOT_WITHIN_ACCEPTABLE_RANGE_CONCATINATION.toString());
- }
- errorFree = false;
- }
- if (!portRange.contains(portRangeEnd)) {
- if (backendInstanceController.portRangeEndIssue.getText().isBlank()) {
- backendInstanceController.portRangeEndIssue.setText(ValidationErrorMessages.PORT_VALUE_NOT_WITHIN_ACCEPTABLE_RANGE.toString());
- } else {
- backendInstanceController.portRangeEndIssue.setText(ValidationErrorMessages.PORT_VALUE_NOT_WITHIN_ACCEPTABLE_RANGE_CONCATINATION.toString());
- }
- errorFree = false;
- }
-
- if (portRangeEnd - portRangeStart < 0) {
- backendInstanceController.portRangeIssue.setText(ValidationErrorMessages.PORT_RANGE_MUST_BE_INCREMENTAL.toString());
- errorFree = false;
- }
-
- backendInstanceController.portRangeStartIssue.setVisible(!errorFree);
- backendInstanceController.portRangeEndIssue.setVisible(!errorFree);
- backendInstanceController.portRangeIssue.setVisible(!errorFree);
-
- return errorFree;
- }
-
- private boolean backendInstanceLocationIsErrorFree(BackendInstanceController backendInstanceController) {
- boolean errorFree = true;
-
- if (backendInstanceController.isLocal.isSelected()) {
- if (backendInstanceController.pathToBackend.getText().isBlank()) {
- backendInstanceController.locationIssue.setText(ValidationErrorMessages.FILE_LOCATION_IS_BLANK.toString());
- errorFree = false;
- } else {
- Path localBackendPath = Paths.get(backendInstanceController.pathToBackend.getText());
-
- if (!Files.isExecutable(localBackendPath)) {
- backendInstanceController.locationIssue.setText(ValidationErrorMessages.FILE_DOES_NOT_EXIST_OR_NOT_EXECUTABLE.toString());
- errorFree = false;
- }
- }
- } else {
- if (backendInstanceController.address.getText().isBlank()) {
- backendInstanceController.locationIssue.setText(ValidationErrorMessages.HOST_ADDRESS_IS_BLANK.toString());
- errorFree = false;
- } else {
- try {
- InetAddress address = InetAddress.getByName(backendInstanceController.address.getText());
- boolean reachable = address.isReachable(200);
-
- if (!reachable) {
- backendInstanceController.locationIssue.setText(ValidationErrorMessages.HOST_NOT_REACHABLE.toString());
- errorFree = false;
- }
-
- } catch (UnknownHostException unknownHostException) {
- backendInstanceController.locationIssue.setText(ValidationErrorMessages.UNACCEPTABLE_HOST_NAME.toString());
- errorFree = false;
- } catch (IOException ioException) {
- backendInstanceController.locationIssue.setText(ValidationErrorMessages.IO_EXCEPTION_WITH_HOST.toString());
- errorFree = false;
- }
- }
- }
-
- backendInstanceController.locationIssue.setVisible(!errorFree);
-
- return errorFree;
- }
-
- private enum ValidationErrorMessages {
- BACKEND_NAME_EMPTY {
- @Override
- public String toString() {
- return "The backend name cannot be empty";
- }
- },
- VALUE_NOT_INTEGER {
- @Override
- public String toString() {
- return "Value must be integer";
- }
- },
- PORT_RANGE_MUST_BE_INCREMENTAL {
- @Override
- public String toString() {
- return "Start of port range must be greater than end";
- }
- },
- PORT_VALUE_NOT_WITHIN_ACCEPTABLE_RANGE {
- @Override
- public String toString() {
- return "Value must be within range 0 - 65535";
- }
- },
- PORT_VALUE_NOT_WITHIN_ACCEPTABLE_RANGE_CONCATINATION {
- @Override
- public String toString() {
- return " and within range 0 - 65535";
- }
- },
- FILE_LOCATION_IS_BLANK {
- @Override
- public String toString() {
- return "Please specify a file for this backend";
- }
- },
- FILE_DOES_NOT_EXIST_OR_NOT_EXECUTABLE {
- @Override
- public String toString() {
- return "The above file does not exists or ECDAR does not have the privileges to execute it";
- }
- },
- HOST_ADDRESS_IS_BLANK {
- @Override
- public String toString() {
- return "Please specify an address for the external host";
- }
- },
- HOST_NOT_REACHABLE {
- @Override
- public String toString() {
- return "The above address is not reachable. Make sure that the host is correct";
- }
- },
- UNACCEPTABLE_HOST_NAME {
- @Override
- public String toString() {
- return "The above address is not an acceptable host name";
- }
- },
- IO_EXCEPTION_WITH_HOST {
- @Override
- public String toString() {
- return "An I/O exception was encountered while trying to reach the host";
- }
- }
- }
-}
diff --git a/src/main/java/ecdar/controllers/CanvasController.java b/src/main/java/ecdar/controllers/CanvasController.java
index 76306a29..7427c70a 100644
--- a/src/main/java/ecdar/controllers/CanvasController.java
+++ b/src/main/java/ecdar/controllers/CanvasController.java
@@ -2,11 +2,6 @@
import com.jfoenix.controls.JFXRippler;
import ecdar.Ecdar;
-import ecdar.abstractions.Component;
-import ecdar.abstractions.Declarations;
-import ecdar.abstractions.HighLevelModelObject;
-import ecdar.abstractions.EcdarSystem;
-import ecdar.mutation.models.MutationTestPlan;
import ecdar.mutation.MutationTestPlanPresentation;
import ecdar.presentations.*;
import ecdar.utility.helpers.ZoomHelper;
@@ -39,8 +34,8 @@ public class CanvasController implements Initializable {
public final double DECLARATION_Y_MARGIN = Ecdar.CANVAS_PADDING * 5.5;
public ComponentPresentation activeComponentPresentation;
- private final ObjectProperty activeModel = new SimpleObjectProperty<>(null);
- private final HashMap> ModelObjectTranslateMap = new HashMap<>();
+ private final ObjectProperty activeModelPresentation = new SimpleObjectProperty<>(null);
+ private final HashMap> ModelObjectTranslateMap = new HashMap<>();
private DoubleProperty width, height;
private BooleanProperty insetShouldShow;
@@ -56,21 +51,22 @@ public BooleanProperty getInsetShouldShow() {
return insetShouldShow;
}
- public HighLevelModelObject getActiveModel() {
- return activeModel.get();
+ public HighLevelModelPresentation getActiveModelPresentation() {
+ return activeModelPresentation.get();
}
/**
* Sets the given model as the one to be active, e.g. to be shown on the screen.
* @param model the given model
*/
- public void setActiveModel(final HighLevelModelObject model) {
- activeModel.set(model);
- Platform.runLater(EcdarController.getActiveCanvasPresentation().getController()::leaveTextAreas);
+ public void setActiveModelPresentation(final HighLevelModelPresentation model) {
+ activeModelPresentation.set(model);
+ Platform.runLater(Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasPresentation().getController()::leaveTextAreas);
+ Platform.runLater(zoomHelper::zoomToFit);
}
- public ObjectProperty activeComponentProperty() {
- return activeModel;
+ public ObjectProperty activeModelProperty() {
+ return activeModelPresentation;
}
public void leaveTextAreas() {
@@ -101,9 +97,11 @@ public void initialize(final URL location, final ResourceBundle resources) {
root.widthProperty().addListener((observable, oldValue, newValue) -> width.setValue(newValue));
root.heightProperty().addListener((observable, oldValue, newValue) -> height.setValue(newValue));
- activeModel.addListener((obs, oldModel, newModel) -> onActiveModelChanged(oldModel, newModel));
+ activeModelPresentation.addListener((obs, oldModel, newModel) -> {
+ onActiveModelChanged(oldModel, newModel);
+ });
- leaveTextAreas = () -> root.requestFocus();
+ Platform.runLater(() -> leaveTextAreas = () -> root.requestFocus());
leaveOnEnterPressed = (keyEvent) -> {
if (keyEvent.getCode().equals(KeyCode.ENTER) || keyEvent.getCode().equals(KeyCode.ESCAPE)) {
leaveTextAreas();
@@ -130,9 +128,9 @@ public void updateOffset(final Boolean shouldHave) {
* @param oldObject old object
* @param newObject new object
*/
- private void onActiveModelChanged(final HighLevelModelObject oldObject, final HighLevelModelObject newObject) {
+ private void onActiveModelChanged(final HighLevelModelPresentation oldObject, final HighLevelModelPresentation newObject) {
// If old object is a component or system, add to map in order to remember coordinate
- if (oldObject instanceof Component || oldObject instanceof EcdarSystem) {
+ if (oldObject instanceof ComponentPresentation || oldObject instanceof SystemPresentation) {
ModelObjectTranslateMap.put(oldObject, new Pair<>(modelPane.getTranslateX(), modelPane.getTranslateY()));
}
@@ -142,23 +140,34 @@ private void onActiveModelChanged(final HighLevelModelObject oldObject, final Hi
// Remove old object from view
modelPane.getChildren().removeIf(node -> node instanceof HighLevelModelPresentation);
- if (newObject instanceof Component) {
- activeComponentPresentation = new ComponentPresentation((Component) newObject);
+ if (newObject instanceof ComponentPresentation) {
+ activeComponentPresentation = (ComponentPresentation) newObject;
modelPane.getChildren().add(activeComponentPresentation);
- } else if (newObject instanceof Declarations) {
+
+ // To avoid NullPointerException on initial model
+ Platform.runLater(zoomHelper::resetZoom);
+
+ } else if (newObject instanceof DeclarationsPresentation) {
activeComponentPresentation = null;
- modelPane.getChildren().add(new DeclarationPresentation((Declarations) newObject));
- } else if (newObject instanceof EcdarSystem) {
+ modelPane.getChildren().add(newObject);
+
+ // Bind size of Declaration to size of the model pane to ensure alignment and avoid drag
+ DeclarationsController declarationsController = (DeclarationsController) newObject.getController();
+ declarationsController.root.minWidthProperty().bind(modelPane.minWidthProperty());
+ declarationsController.root.maxWidthProperty().bind(modelPane.maxWidthProperty());
+ declarationsController.root.minHeightProperty().bind(modelPane.minHeightProperty());
+ declarationsController.root.maxHeightProperty().bind(modelPane.maxHeightProperty());
+ } else if (newObject instanceof SystemPresentation) {
activeComponentPresentation = null;
- modelPane.getChildren().add(new SystemPresentation((EcdarSystem) newObject));
- } else if (newObject instanceof MutationTestPlan) {
+ modelPane.getChildren().add(newObject);
+ } else if (newObject instanceof MutationTestPlanPresentation) {
activeComponentPresentation = null;
- modelPane.getChildren().add(new MutationTestPlanPresentation((MutationTestPlan) newObject));
+ modelPane.getChildren().add(newObject);
} else {
throw new IllegalStateException("Type of object is not supported.");
}
- boolean shouldZoomBeActive = newObject instanceof Component || newObject instanceof EcdarSystem;
+ boolean shouldZoomBeActive = newObject instanceof ComponentPresentation || newObject instanceof SystemPresentation;
setZoomAvailable(shouldZoomBeActive);
root.requestFocus();
diff --git a/src/main/java/ecdar/controllers/ComponentController.java b/src/main/java/ecdar/controllers/ComponentController.java
index a357c1a6..49fd610f 100644
--- a/src/main/java/ecdar/controllers/ComponentController.java
+++ b/src/main/java/ecdar/controllers/ComponentController.java
@@ -6,26 +6,32 @@
import ecdar.code_analysis.CodeAnalysis;
import ecdar.presentations.*;
import ecdar.utility.UndoRedoStack;
-import ecdar.utility.colors.Color;
+import ecdar.utility.colors.EnabledColor;
import ecdar.utility.helpers.*;
-import ecdar.utility.mouse.MouseTracker;
import com.jfoenix.controls.JFXPopup;
import com.jfoenix.controls.JFXRippler;
+import ecdar.utility.keyboard.NudgeDirection;
import javafx.animation.Interpolator;
import javafx.animation.Transition;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
+import javafx.geometry.Insets;
+import javafx.geometry.Point2D;
+import javafx.geometry.Pos;
+import javafx.scene.Cursor;
import javafx.scene.layout.*;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
+import javafx.scene.shape.Path;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.Shape;
import javafx.util.Duration;
import org.fxmisc.richtext.LineNumberFactory;
import org.fxmisc.richtext.StyleClassedTextArea;
@@ -36,15 +42,20 @@
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static ecdar.presentations.ModelPresentation.*;
+import static ecdar.utility.helpers.UnoccupiedSpaceFinder.getUnoccupiedSpace;
public class ComponentController extends ModelController implements Initializable {
+ private final List> updateColorDelegates = new ArrayList<>();
private static final Map> locationListChangeListenerMap = new HashMap<>();
private static final Map errorsAndWarningsInitialized = new HashMap<>();
- private static Location placingLocation = null;
private final ObjectProperty component = new SimpleObjectProperty<>(null);
private final Map edgePresentationMap = new HashMap<>();
private final Map locationPresentationMap = new HashMap<>();
+ // View elements
public StyleClassedTextArea declarationTextArea;
public JFXRippler toggleDeclarationButton;
public Label x;
@@ -56,33 +67,35 @@ public class ComponentController extends ModelController implements Initializabl
public VBox outputSignatureContainer;
public VBox inputSignatureContainer;
- private MouseTracker mouseTracker;
private DropDownMenu contextMenu;
private DropDownMenu finishEdgeContextMenu;
-
- public static boolean isPlacingLocation() {
- return placingLocation != null;
- }
-
- public static void setPlacingLocation(final Location placingLocation) {
- ComponentController.placingLocation = placingLocation;
- }
+ private Point2D dropdownCoordinatesInComponent;
@Override
public void initialize(final URL location, final ResourceBundle resources) {
declarationTextArea.setParagraphGraphicFactory(LineNumberFactory.get(declarationTextArea));
component.addListener((obs, oldComponent, newComponent) -> {
+ super.initialize(newComponent.getBox());
+
+ initializeFrame();
+ initializeToolbar();
+ initializeBackground();
+
+ // Re-run initialisation on update of width and height property
+ newComponent.getBox().getWidthProperty().addListener(observable -> initializeFrame());
+ newComponent.getBox().getHeightProperty().addListener(observable -> initializeFrame());
+
inputSignatureContainer.heightProperty().addListener((change) -> updateMaxHeight());
outputSignatureContainer.heightProperty().addListener((change) -> updateMaxHeight());
- // Bind the declarations of the abstraction the the view
+ // Bind the declarations of the abstraction to the view
declarationTextArea.replaceText(0, declarationTextArea.getLength(), newComponent.getDeclarationsText());
declarationTextArea.textProperty().addListener((observable, oldDeclaration, newDeclaration) -> newComponent.setDeclarationsText(newDeclaration));
// Find the clocks in the decls
newComponent.declarationsTextProperty().addListener((observable, oldValue, newValue) -> {
- final List clocks = new ArrayList();
+ final List clocks = new ArrayList<>();
final String strippedDecls = newValue.replaceAll("[\\r\\n]+", "");
@@ -90,7 +103,7 @@ public void initialize(final URL location, final ResourceBundle resources) {
Matcher matcher = pattern.matcher(strippedDecls);
while (matcher.find()) {
- final String clockStrings[] = matcher.group("CLOCKS").split(",");
+ final String[] clockStrings = matcher.group("CLOCKS").split(",");
for (String clockString : clockStrings) {
clocks.add(clockString.replaceAll("\\s", ""));
}
@@ -99,48 +112,126 @@ public void initialize(final URL location, final ResourceBundle resources) {
//TODO this logs the clocks System.out.println(clocks);
});
- initializeEdgeHandling(newComponent);
- initializeLocationHandling(newComponent);
+ initializeEdgeHandling();
+ initializeLocationHandling();
initializeDeclarations();
- initializeSignature(newComponent);
- initializeSignatureListeners(newComponent);
- });
+ initializeSignature();
+ initializeSignatureListeners();
+ if (!errorsAndWarningsInitialized.containsKey(newComponent) || !errorsAndWarningsInitialized.get(newComponent)) {
+ initializeNoIncomingEdgesWarning();
+ errorsAndWarningsInitialized.put(newComponent, true);
+ }
+ });
- // The root view have been inflated, initialize the mouse tracker on it
- mouseTracker = new MouseTracker(root);
initializeContextMenu();
- component.addListener((obs, old, component) -> {
- if (component == null) return;
+ declarationTextArea.textProperty().addListener((obs, oldText, newText) ->
+ declarationTextArea.setStyleSpans(0, UPPAALSyntaxHighlighter.computeHighlighting(newText)));
+ }
- if (!errorsAndWarningsInitialized.containsKey(component) || !errorsAndWarningsInitialized.get(component)) {
- initializeNoIncomingEdgesWarning();
- errorsAndWarningsInitialized.put(component, true);
- }
+ private void initializeToolbar() {
+ final Consumer updateColor = (newColor) -> {
+ // Set the background of the toolbar
+ toolbar.setBackground(new Background(new BackgroundFill(
+ newColor.color.getColor(newColor.intensity),
+ CornerRadii.EMPTY,
+ Insets.EMPTY
+ )));
+
+ // Set the icon color and rippler color of the toggleDeclarationButton
+ toggleDeclarationButton.setRipplerFill(newColor.getTextColor());
+
+ toolbar.setPrefHeight(TOOLBAR_HEIGHT);
+ toggleDeclarationButton.setBackground(Background.EMPTY);
+ };
+
+ updateColorDelegates.add(updateColor);
+
+ getComponent().colorProperty().addListener(observable -> updateColor.accept(getComponent().getColor()));
+
+ updateColor.accept(getComponent().getColor());
+
+ // Set a hover effect for the controller.toggleDeclarationButton
+ toggleDeclarationButton.setOnMouseEntered(event -> toggleDeclarationButton.setCursor(Cursor.HAND));
+ toggleDeclarationButton.setOnMouseExited(event -> toggleDeclarationButton.setCursor(Cursor.DEFAULT));
+ toggleDeclarationButton.setOnMousePressed(this::toggleDeclaration);
+ }
+
+ private void initializeFrame() {
+ final Shape[] mask = new Shape[1];
+ final Rectangle rectangle = new Rectangle(getComponent().getBox().getWidth(), getComponent().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);
+ frame.setClip(mask[0]);
+ background.setClip(Path.union(mask[0], mask[0]));
+ background.setOpacity(0.5);
+
+ // Bind the missing lines that we cropped away
+ topLeftLine.setStartX(CORNER_SIZE);
+ topLeftLine.setStartY(0);
+ topLeftLine.setEndX(0);
+ topLeftLine.setEndY(CORNER_SIZE);
+ topLeftLine.setStroke(newColor.getStrokeColor());
+ topLeftLine.setStrokeWidth(1.25);
+ StackPane.setAlignment(topLeftLine, Pos.TOP_LEFT);
+
+ // Set the stroke color to two shades darker
+ frame.setBorder(new Border(new BorderStroke(
+ newColor.getStrokeColor(),
+ BorderStrokeStyle.SOLID,
+ CornerRadii.EMPTY,
+ new BorderWidths(1),
+ Insets.EMPTY
+ )));
+ };
+
+ updateColorDelegates.add(updateColor);
+
+ getComponent().colorProperty().addListener(observable -> {
+ updateColor.accept(getComponent().getColor());
+ });
+
+ updateColor.accept(getComponent().getColor());
+ }
+
+ private void initializeBackground() {
+ // Bind the background width and height to the values in the model
+ background.widthProperty().bindBidirectional(getComponent().getBox().getWidthProperty());
+ background.heightProperty().bindBidirectional(getComponent().getBox().getHeightProperty());
+
+ final Consumer updateColor = (newColor) -> {
+ // Set the background color to the lightest possible version of the color and then increase by two
+ background.setFill(newColor.getLowestIntensity().nextIntensity(2).getPaintColor());
+ };
+
+ updateColorDelegates.add(updateColor);
+
+ getComponent().colorProperty().addListener(observable -> {
+ updateColor.accept(getComponent().getColor());
});
+
+ updateColor.accept(getComponent().getColor());
}
/***
* Inserts the initial edges of the component to the input/output signature
- * @param newComponent The component that should be presented with its signature
*/
- private void initializeSignature(final Component newComponent) {
- newComponent.getOutputStrings().forEach((channel) -> insertSignatureArrow(channel, EdgeStatus.OUTPUT));
- newComponent.getInputStrings().forEach((channel) -> insertSignatureArrow(channel, EdgeStatus.INPUT));
+ private void initializeSignature() {
+ getComponent().getOutputStrings().forEach((channel) -> insertSignatureArrow(channel, EdgeStatus.OUTPUT));
+ 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
- * @param newComponent The component that should be presented with its signature
*/
-
-
- private void initializeSignatureListeners(final Component newComponent) {
- newComponent.getIsFailingProperty().addListener((observable, oldValue, newValue) -> {
- if (newComponent.getIsFailing()) {
+ 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()) {
@@ -172,7 +263,7 @@ private void initializeSignatureListeners(final Component newComponent) {
}
}
});
- newComponent.getOutputStrings().addListener((ListChangeListener) c -> {
+ 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();
while (c.next()) {
@@ -180,7 +271,7 @@ private void initializeSignatureListeners(final Component newComponent) {
}
});
- newComponent.getInputStrings().addListener((ListChangeListener) c -> {
+ getComponent().getInputStrings().addListener((ListChangeListener) c -> {
inputSignatureContainer.getChildren().clear();
while (c.next()) {
c.getAddedSubList().forEach((channel) -> insertSignatureArrow(channel, EdgeStatus.INPUT));
@@ -188,35 +279,6 @@ private void initializeSignatureListeners(final Component newComponent) {
});
}
- /***
- * Inserts a new {@link ecdar.presentations.SignatureArrow} in the containers for either input or output signature
- * @param channel A String with the channel name that should be shown with the arrow
- * @param status An EdgeStatus for the type of arrow to insert
- */
- private void insertSignatureArrow(final String channel, final EdgeStatus status) {
- SignatureArrow newArrow = new SignatureArrow(channel, status, component.get());
-
- if (status == EdgeStatus.INPUT) {
- inputSignatureContainer.getChildren().add(newArrow);
- } else {
- outputSignatureContainer.getChildren().add(newArrow);
- }
-
- }
-
- /***
- * Updates the component's height to match the input/output signature containers
- * if the component is smaller than either of them
- */
- private void updateMaxHeight() {
- // If input/outputsignature container is taller than the current component height
- // we update the component's height to be as tall as the container
- double maxHeight = findMaxHeight();
- if (maxHeight > component.get().getBox().getHeight()) {
- component.get().getBox().getHeightProperty().set(maxHeight);
- }
- }
-
/***
* Finds the max height of the input/output signature container and the component
* @return a double of the largest height
@@ -283,26 +345,19 @@ private void initializeNoIncomingEdgesWarning() {
}
};
- final Component component = getComponent();
- checkLocations.accept(component);
+ checkLocations.accept(getComponent());
// Check location whenever we get new edges
- component.getDisplayableEdges().addListener(new ListChangeListener() {
- @Override
- public void onChanged(final Change extends DisplayableEdge> c) {
- while (c.next()) {
- checkLocations.accept(component);
- }
+ getComponent().getDisplayableEdges().addListener((ListChangeListener) c -> {
+ while (c.next()) {
+ checkLocations.accept(getComponent());
}
});
// Check location whenever we get new locations
- component.getLocations().addListener(new ListChangeListener() {
- @Override
- public void onChanged(final Change extends Location> c) {
- while (c.next()) {
- checkLocations.accept(component);
- }
+ getComponent().getLocations().addListener((ListChangeListener) c -> {
+ while (c.next()) {
+ checkLocations.accept(getComponent());
}
});
}
@@ -313,21 +368,12 @@ private void initializeContextMenu() {
return;
}
- contextMenu = new DropDownMenu(root);
+ contextMenu = new DropDownMenu(modelContainerSubComponent);
contextMenu.addClickableListElement("Add Location", event -> {
contextMenu.hide();
- final Location newLocation = new Location();
- newLocation.initialize();
-
- double x = DropDownMenu.x - LocationPresentation.RADIUS / 2;
- newLocation.setX(x);
-
- double y = DropDownMenu.y - LocationPresentation.RADIUS / 2;
- newLocation.setY(y);
- newLocation.setColorIntensity(component.getColorIntensity());
- newLocation.setColor(component.getColor());
+ final Location newLocation = new Location(component, Location.Type.NORMAL, getComponent().getUniqueLocationId(), dropdownCoordinatesInComponent.getX(), dropdownCoordinatesInComponent.getY());
// Add a new location
UndoRedoStack.pushAndPerform(() -> { // Perform
@@ -337,13 +383,10 @@ private void initializeContextMenu() {
}, "Added location '" + newLocation + "' to component '" + component.getName() + "'", "add-circle");
});
- // Adds the add universal location element to the drop down menu, this element adds an universal location and its required edges
+ // Adds the add universal location element to the drop-down menu, this element adds a universal location and its required edges
contextMenu.addClickableListElement("Add Universal Location", event -> {
contextMenu.hide();
- double x = DropDownMenu.x - LocationPresentation.RADIUS / 2;
- double y = DropDownMenu.y - LocationPresentation.RADIUS / 2;
-
- final Location newLocation = new Location(component, Location.Type.UNIVERSAL, x, y);
+ final Location newLocation = new Location(component, Location.Type.UNIVERSAL, getComponent().getUniqueLocationId(), dropdownCoordinatesInComponent.getX(), dropdownCoordinatesInComponent.getY());
final Edge inputEdge = newLocation.addLeftEdge("*", EdgeStatus.INPUT);
inputEdge.setIsLocked(true);
@@ -366,10 +409,7 @@ private void initializeContextMenu() {
// Adds the add inconsistent location element to the dropdown menu, this element adds an inconsistent location
contextMenu.addClickableListElement("Add Inconsistent Location", event -> {
contextMenu.hide();
- double x = DropDownMenu.x - LocationPresentation.RADIUS / 2;
- double y = DropDownMenu.y - LocationPresentation.RADIUS / 2;
-
- final Location newLocation = new Location(component, Location.Type.INCONSISTENT, x, y);
+ final Location newLocation = new Location(component, Location.Type.INCONSISTENT, getComponent().getUniqueLocationId(), dropdownCoordinatesInComponent.getX(), dropdownCoordinatesInComponent.getY());
// Add a new location
UndoRedoStack.pushAndPerform(() -> { // Perform
@@ -379,24 +419,6 @@ private void initializeContextMenu() {
}, "Added inconsistent location '" + newLocation + "' to component '" + component.getName() + "'", "add-circle");
});
- contextMenu.addSpacerElement();
-
- contextMenu.addClickableListElement("Contains deadlock?", event -> {
-
- // Generate the query
- final String deadlockQuery = BackendHelper.getExistDeadlockQuery(getComponent());
-
- // Add proper comment
- final String deadlockComment = "Does " + component.getName() + " contain a deadlock?";
-
- // Add new query for this component
- final Query query = new Query(deadlockQuery, deadlockComment, QueryState.UNKNOWN);
- query.setType(QueryType.REACHABILITY);
- Ecdar.getProject().getQueries().add(query);
- Ecdar.getQueryExecutor().executeQuery(query);
- contextMenu.hide();
- });
-
contextMenu.addSpacerElement();
contextMenu.addColorPicker(component, component::dye);
};
@@ -405,13 +427,6 @@ private void initializeContextMenu() {
initializeDropDownMenu.accept(newComponent);
});
- Ecdar.getProject().getComponents().addListener(new ListChangeListener() {
- @Override
- public void onChanged(final Change extends Component> c) {
- initializeDropDownMenu.accept(getComponent());
- }
- });
-
initializeDropDownMenu.accept(getComponent());
}
@@ -429,15 +444,13 @@ private void initializeFinishEdgeContextMenu(final DisplayableEdge unfinishedEdg
locationAware.yProperty().set(y);
};
- finishEdgeContextMenu = new DropDownMenu(root);
+ finishEdgeContextMenu = new DropDownMenu(modelContainerSubComponent);
finishEdgeContextMenu.addListElement("Finish edge in a:");
finishEdgeContextMenu.addClickableListElement("Location", event -> {
finishEdgeContextMenu.hide();
final Location location = new Location();
- location.initialize();
-
- location.setColorIntensity(getComponent().getColorIntensity());
+ location.initialize(getComponent().getUniqueLocationId());
location.setColor(getComponent().getColor());
if (component.isAnyEdgeWithoutSource()) {
@@ -466,10 +479,7 @@ private void initializeFinishEdgeContextMenu(final DisplayableEdge unfinishedEdg
finishEdgeContextMenu.addClickableListElement("Universal Location", event -> {
finishEdgeContextMenu.hide();
- double x = DropDownMenu.x - LocationPresentation.RADIUS / 2;
- double y = DropDownMenu.y - LocationPresentation.RADIUS / 2;
-
- final Location newLocation = new Location(component, Location.Type.UNIVERSAL, x, y);
+ final Location newLocation = new Location(component, Location.Type.UNIVERSAL, getComponent().getUniqueLocationId(), dropdownCoordinatesInComponent.getX(), dropdownCoordinatesInComponent.getY());
final Edge inputEdge = newLocation.addLeftEdge("*", EdgeStatus.INPUT);
inputEdge.setIsLocked(true);
@@ -508,10 +518,7 @@ private void initializeFinishEdgeContextMenu(final DisplayableEdge unfinishedEdg
finishEdgeContextMenu.addClickableListElement("Inconsistent Location", event -> {
finishEdgeContextMenu.hide();
- double x = DropDownMenu.x - LocationPresentation.RADIUS / 2;
- double y = DropDownMenu.y - LocationPresentation.RADIUS / 2;
-
- final Location newLocation = new Location(component, Location.Type.INCONSISTENT, x, y);
+ final Location newLocation = new Location(component, Location.Type.INCONSISTENT, getComponent().getUniqueLocationId(), dropdownCoordinatesInComponent.getX(), dropdownCoordinatesInComponent.getY());
if (component.isAnyEdgeWithoutSource()) {
unfinishedEdge.setSourceLocation(newLocation);
@@ -544,218 +551,15 @@ private void initializeFinishEdgeContextMenu(final DisplayableEdge unfinishedEdg
initializeDropDownMenu.accept(getComponent());
}
- private void initializeLocationHandling(final Component newComponent) {
- final Consumer handleAddedLocation = (loc) -> {
- // Check related to undo/redo stack
- if (locationPresentationMap.containsKey(loc)) {
- return;
- }
-
- // Create a new presentation, and register it on the map
- final LocationPresentation newLocationPresentation = new LocationPresentation(loc, newComponent);
-
- final ChangeListener locationPlacementChangedListener = (observable, oldValue, newValue) -> {
- final double offset = newLocationPresentation.getController().circle.getRadius() * 2;
- boolean hit = false;
- ItemDragHelper.DragBounds componentBounds = newLocationPresentation.getController().getDragBounds();
-
- //Define the x and y coordinates for the initial and final locations
- final double initialLocationX = component.get().getBox().getX() + newLocationPresentation.getController().circle.getRadius() * 2,
- initialLocationY = component.get().getBox().getY() + newLocationPresentation.getController().circle.getRadius() * 2,
- finalLocationX = component.get().getBox().getX() + component.get().getBox().getWidth() - newLocationPresentation.getController().circle.getRadius() * 2,
- finalLocationY = component.get().getBox().getY() + component.get().getBox().getHeight() - newLocationPresentation.getController().circle.getRadius() * 2;
-
- double latestHitRight = 0,
- latestHitDown = 0,
- latestHitLeft = 0,
- latestHitUp = 0;
-
- //Check to see if the location is placed on top of the initial location
- if (Math.abs(initialLocationX - (newLocationPresentation.getLayoutX())) < offset &&
- Math.abs(initialLocationY - (newLocationPresentation.getLayoutY())) < offset) {
- hit = true;
- latestHitRight = initialLocationX;
- latestHitDown = initialLocationY;
- latestHitLeft = initialLocationX;
- latestHitUp = initialLocationY;
- }
-
- //Check to see if the location is placed on top of the final location
- else if (Math.abs(finalLocationX - (newLocationPresentation.getLayoutX())) < offset &&
- Math.abs(finalLocationY - (newLocationPresentation.getLayoutY())) < offset) {
- hit = true;
- latestHitRight = finalLocationX;
- latestHitDown = finalLocationY;
- latestHitLeft = finalLocationX;
- latestHitUp = finalLocationY;
- }
-
- //Check to see if the location is placed on top of another location
- else {
- for (Map.Entry entry : locationPresentationMap.entrySet()) {
- if (entry.getValue() != newLocationPresentation &&
- Math.abs(entry.getValue().getLayoutX() - (newLocationPresentation.getLayoutX())) < offset &&
- Math.abs(entry.getValue().getLayoutY() - (newLocationPresentation.getLayoutY())) < offset) {
- hit = true;
- latestHitRight = entry.getValue().getLayoutX();
- latestHitDown = entry.getValue().getLayoutY();
- latestHitLeft = entry.getValue().getLayoutX();
- latestHitUp = entry.getValue().getLayoutY();
- break;
- }
- }
- }
-
- //If the location is not placed on top of any other locations, do not do anything
- if (!hit) {
- return;
- }
- hit = false;
-
- //Find an unoccupied space for the location
- for (int i = 1; i < component.get().getBox().getWidth() / offset; i++) {
-
- //Check to see, if the location can be placed to the right of the existing locations
- if (componentBounds.trimX(latestHitRight + offset) == latestHitRight + offset) {
-
- //Check if the location would be placed on the final location
- if (Math.abs(finalLocationX - (latestHitRight + offset)) < offset &&
- Math.abs(finalLocationY - (newLocationPresentation.getLayoutY())) < offset) {
- hit = true;
- latestHitRight = finalLocationX;
- } else {
- for (Map.Entry entry : locationPresentationMap.entrySet()) {
- if (entry.getValue() != newLocationPresentation &&
- Math.abs(entry.getValue().getLayoutX() - (latestHitRight + offset)) < offset &&
- Math.abs(entry.getValue().getLayoutY() - (newLocationPresentation.getLayoutY())) < offset) {
- hit = true;
- latestHitRight = entry.getValue().getLayoutX();
- break;
- }
- }
- }
-
- if (!hit) {
- newLocationPresentation.setLayoutX(latestHitRight + offset);
- return;
- }
- }
- hit = false;
-
- //Check to see, if the location can be placed below the existing locations
- if (componentBounds.trimY(latestHitDown + offset) == latestHitDown + offset) {
-
- //Check if the location would be placed on the final location
- if (Math.abs(finalLocationX - (newLocationPresentation.getLayoutX())) < offset &&
- Math.abs(finalLocationY - (latestHitDown + offset)) < offset) {
- hit = true;
- latestHitDown = finalLocationY;
- } else {
- for (Map.Entry entry : locationPresentationMap.entrySet()) {
- if (entry.getValue() != newLocationPresentation &&
- Math.abs(entry.getValue().getLayoutX() - (newLocationPresentation.getLayoutX())) < offset &&
- Math.abs(entry.getValue().getLayoutY() - (latestHitDown + offset)) < offset) {
- hit = true;
- latestHitDown = entry.getValue().getLayoutY();
- break;
- }
- }
- }
- if (!hit) {
- newLocationPresentation.setLayoutY(latestHitDown + offset);
- return;
- }
- }
- hit = false;
-
- //Check to see, if the location can be placed to the left of the existing locations
- if (componentBounds.trimX(latestHitLeft - offset) == latestHitLeft - offset) {
-
- //Check if the location would be placed on the initial location
- if (Math.abs(initialLocationX - (latestHitLeft - offset)) < offset &&
- Math.abs(initialLocationY - (newLocationPresentation.getLayoutY())) < offset) {
- hit = true;
- latestHitLeft = initialLocationX;
- } else {
- for (Map.Entry entry : locationPresentationMap.entrySet()) {
- if (entry.getValue() != newLocationPresentation &&
- Math.abs(entry.getValue().getLayoutX() - (latestHitLeft - offset)) < offset &&
- Math.abs(entry.getValue().getLayoutY() - (newLocationPresentation.getLayoutY())) < offset) {
- hit = true;
- latestHitLeft = entry.getValue().getLayoutX();
- break;
- }
- }
- }
- if (!hit) {
- newLocationPresentation.setLayoutX(latestHitLeft - offset);
- return;
- }
- }
- hit = false;
-
- //Check to see, if the location can be placed above the existing locations
- if (componentBounds.trimY(latestHitUp - offset) == latestHitUp - offset) {
-
- //Check if the location would be placed on the initial location
- if (Math.abs(initialLocationX - (newLocationPresentation.getLayoutX())) < offset &&
- Math.abs(initialLocationY - (latestHitUp - offset)) < offset) {
- hit = true;
- latestHitUp = initialLocationY;
- } else {
- for (Map.Entry entry : locationPresentationMap.entrySet()) {
- if (entry.getValue() != newLocationPresentation &&
- Math.abs(entry.getValue().getLayoutX() - (newLocationPresentation.getLayoutX())) < offset &&
- Math.abs(entry.getValue().getLayoutY() - (latestHitUp - offset)) < offset) {
- hit = true;
- latestHitUp = entry.getValue().getLayoutY();
- break;
- }
- }
- }
- if (!hit) {
- newLocationPresentation.setLayoutY(latestHitUp - offset);
- return;
- }
- }
- hit = false;
- }
- modelContainerLocation.getChildren().remove(newLocationPresentation);
- locationPresentationMap.remove(newLocationPresentation.getController().locationProperty().getValue());
- newComponent.getLocations().remove(newLocationPresentation.getController().getLocation());
- newComponent.getDisplayableEdges().removeIf(e -> e.targetLocationProperty().get().equals(newLocationPresentation.getController().getLocation()));
- Ecdar.showToast("Please select an empty space for the new location");
- };
-
- newLocationPresentation.layoutXProperty().addListener(locationPlacementChangedListener);
- newLocationPresentation.layoutYProperty().addListener(locationPlacementChangedListener);
-
- locationPresentationMap.put(loc, newLocationPresentation);
-
- // Add it to the view
- modelContainerLocation.getChildren().add(newLocationPresentation);
-
- // Bind the newly created location to the mouse and tell the ui that it is not placed yet
- if (loc.getX() == 0) {
- newLocationPresentation.setPlaced(false);
- BindingHelper.bind(loc, newComponent.getBox().getXProperty(), newComponent.getBox().getYProperty());
- }
- };
-
+ private void initializeLocationHandling() {
final ListChangeListener locationListChangeListener = c -> {
if (c.next()) {
// Locations are added to the component
c.getAddedSubList().forEach((loc) -> {
- handleAddedLocation.accept(loc);
-
- LocationPresentation locationPresentation = locationPresentationMap.get(loc);
-
- //Ensure that the component is inside the bounds of the component
- locationPresentation.setLayoutX(locationPresentation.getController().getDragBounds().trimX(locationPresentation.getLayoutX()));
- locationPresentation.setLayoutY(locationPresentation.getController().getDragBounds().trimY(locationPresentation.getLayoutY()));
-
- //Change the layoutXProperty slightly to invoke listener and ensure distance to existing locations
- locationPresentation.setLayoutX(locationPresentation.getLayoutX() + 0.00001);
+ // Check related to undo/redo stack
+ if (!locationPresentationMap.containsKey(loc)) {
+ addLocation(loc);
+ }
});
// Locations are removed from the component
@@ -766,18 +570,19 @@ else if (Math.abs(finalLocationX - (newLocationPresentation.getLayoutX())) < off
});
}
};
- newComponent.getLocations().addListener(locationListChangeListener);
- if (!locationListChangeListenerMap.containsKey(newComponent)) {
- locationListChangeListenerMap.put(newComponent, locationListChangeListener);
+ getComponent().getLocations().addListener(locationListChangeListener);
+
+ if (!locationListChangeListenerMap.containsKey(getComponent())) {
+ locationListChangeListenerMap.put(getComponent(), locationListChangeListener);
}
- newComponent.getLocations().forEach(handleAddedLocation);
+ getComponent().getLocations().forEach(this::addLocation);
}
- private void initializeEdgeHandling(final Component newComponent) {
+ private void initializeEdgeHandling() {
final Consumer handleAddedEdge = edge -> {
- final EdgePresentation edgePresentation = new EdgePresentation(edge, newComponent);
+ final EdgePresentation edgePresentation = new EdgePresentation(edge, getComponent());
edgePresentationMap.put(edge, edgePresentation);
modelContainerEdge.getChildren().add(edgePresentation);
@@ -790,28 +595,26 @@ private void initializeEdgeHandling(final Component newComponent) {
};
// React on addition of edges to the component
- newComponent.getDisplayableEdges().addListener(new ListChangeListener() {
- @Override
- public void onChanged(final Change extends DisplayableEdge> c) {
- if (c.next()) {
- // Edges are added to the component
- c.getAddedSubList().forEach(handleAddedEdge);
-
- // Edges are removed from the component
- c.getRemoved().forEach(edge -> {
- final EdgePresentation edgePresentation = edgePresentationMap.get(edge);
- modelContainerEdge.getChildren().remove(edgePresentation);
- edgePresentationMap.remove(edge);
- });
- }
+ getComponent().getDisplayableEdges().addListener((ListChangeListener) c -> {
+ if (c.next()) {
+ // Edges are added to the component
+ c.getAddedSubList().forEach(handleAddedEdge);
+
+ // Edges are removed from the component
+ c.getRemoved().forEach(edge -> {
+ final EdgePresentation edgePresentation = edgePresentationMap.get(edge);
+ modelContainerEdge.getChildren().remove(edgePresentation);
+ edgePresentationMap.remove(edge);
+ });
}
});
- newComponent.getDisplayableEdges().forEach(handleAddedEdge);
+
+ getComponent().getDisplayableEdges().forEach(handleAddedEdge);
}
private void initializeDeclarations() {
// Initially style the declarations
- declarationTextArea.setStyleSpans(0, ComponentPresentation.computeHighlighting(getComponent().getDeclarationsText()));
+ declarationTextArea.setStyleSpans(0, UPPAALSyntaxHighlighter.computeHighlighting(getComponent().getDeclarationsText()));
declarationTextArea.getStyleClass().add("component-declaration");
final Circle circle = new Circle(0);
@@ -823,9 +626,81 @@ private void initializeDeclarations() {
clip.set(circle);
}
- public void toggleDeclaration(final MouseEvent mouseEvent) {
+ /***
+ * Inserts a new {@link ecdar.presentations.SignatureArrow} in the containers for either input or output signature
+ * @param channel A String with the channel name that should be shown with the arrow
+ * @param status An EdgeStatus for the type of arrow to insert
+ */
+ private void insertSignatureArrow(final String channel, final EdgeStatus status) {
+ SignatureArrow newArrow = new SignatureArrow(channel, status, getComponent());
+ if (status == EdgeStatus.INPUT) {
+ inputSignatureContainer.getChildren().add(newArrow);
+ } else {
+ outputSignatureContainer.getChildren().add(newArrow);
+ }
+ }
+
+ /***
+ * Handles the addition of a new location
+ * @param loc The location to add to the component
+ */
+ private void addLocation(Location loc) {
+ LocationPresentation newLocationPresentation = new LocationPresentation(loc, getComponent());
+ Point2D placement = getUnoccupiedSpace(getComponent().getBox(), locationPresentationMap.values().stream().map(l -> new Point2D(l.getLayoutX(), l.getLayoutY())).collect(Collectors.toList()), new Point2D(loc.getX(), loc.getY()), LocationPresentation.RADIUS * 2 + Ecdar.CANVAS_PADDING);
+
+ if (placement == null) {
+ getComponent().getLocations().remove(loc);
+ Ecdar.showToast("Please select an empty space for the new location");
+ return;
+ }
+
+ newLocationPresentation.setLayoutX(placement.getX());
+ newLocationPresentation.setLayoutY(placement.getY());
+
+ locationPresentationMap.put(loc, newLocationPresentation);
+ modelContainerLocation.getChildren().add(newLocationPresentation);
+
+ // Bind the newly created location to the mouse and tell the ui that it is not placed yet
+ if (loc.getX() == 0) {
+ newLocationPresentation.setPlaced(false);
+ BindingHelper.bind(loc, getComponent().getBox().getXProperty(), getComponent().getBox().getYProperty());
+ }
+ }
+
+ /***
+ * Updates the component's height to match the input/output signature containers
+ * if the component is smaller than either of them
+ */
+ private void updateMaxHeight() {
+ // If input/outputsignature container is taller than the current component height
+ // we update the component's height to be as tall as the container
+ double maxHeight = getMaxHeight();
+ if (maxHeight > getComponent().getBox().getHeight()) {
+ getComponent().getBox().getHeightProperty().set(maxHeight);
+ }
+ }
+
+ /***
+ * Finds the max height of the input/output signature container and the component
+ * @return a double of the largest height
+ */
+ private double getMaxHeight() {
+ double inputHeight = inputSignatureContainer.getHeight();
+ double outputHeight = outputSignatureContainer.getHeight();
+ double componentHeight = getComponent().getBox().getHeight();
+
+ double maxSignatureHeight = Math.max(outputHeight, inputHeight);
+
+ return Math.max(maxSignatureHeight, componentHeight);
+ }
+
+ /***
+ * Toggle the declaration of the component with a ripple effect originating from the MouseEvent
+ * @param mouseEvent to use for the origin of the ripple effect
+ */
+ private void toggleDeclaration(final MouseEvent mouseEvent) {
final Circle circle = new Circle(0);
- circle.setCenterX(component.get().getBox().getWidth() - (toggleDeclarationButton.getWidth() - mouseEvent.getX()));
+ circle.setCenterX(getComponent().getBox().getWidth() - (toggleDeclarationButton.getWidth() - mouseEvent.getX()));
circle.setCenterY(-1 * mouseEvent.getY());
final ObjectProperty clip = new SimpleObjectProperty<>(circle);
@@ -855,6 +730,20 @@ protected void interpolate(final double fraction) {
getComponent().declarationOpenProperty().set(!getComponent().isDeclarationOpen());
}
+ /***
+ * Mark the component as selected in the view
+ */
+ public void componentSelected() {
+ updateColorDelegates.forEach(colorConsumer -> colorConsumer.accept(new EnabledColor(SelectHelper.SELECT_COLOR, SelectHelper.SELECT_COLOR_INTENSITY_NORMAL)));
+ }
+
+ /***
+ * Mark the component as not selected in the view
+ */
+ public void componentDeselected() {
+ updateColorDelegates.forEach(colorConsumer -> colorConsumer.accept(getComponent().getColor()));
+ }
+
public Component getComponent() {
return component.get();
}
@@ -867,19 +756,56 @@ public ObjectProperty componentProperty() {
return component;
}
+ /**
+ * Moves all nodes left.
+ */
+ public void moveAllNodesLeft() {
+ getComponent().getLocations().forEach(loc -> loc.setX(loc.getX() + NudgeDirection.LEFT.getXOffset()));
+ getComponent().getDisplayableEdges().forEach(edge -> edge.getNails().forEach(nail -> nail.setX(nail.getX() + NudgeDirection.LEFT.getXOffset())));
+ }
+
+ /**
+ * Moves all nodes right.
+ */
+ public void moveAllNodesRight() {
+ getComponent().getLocations().forEach(loc -> loc.setX(loc.getX() + NudgeDirection.RIGHT.getXOffset()));
+ getComponent().getDisplayableEdges().forEach(edge -> edge.getNails().forEach(nail -> nail.setX(nail.getX() + NudgeDirection.RIGHT.getXOffset())));
+ }
+
+ /**
+ * Moves all nodes down.
+ */
+ public void moveAllNodesDown() {
+ getComponent().getLocations().forEach(loc -> loc.setY(loc.getY() + NudgeDirection.DOWN.getYOffset()));
+ getComponent().getDisplayableEdges().forEach(edge -> edge.getNails().forEach(nail -> nail.setY(nail.getY() + NudgeDirection.DOWN.getYOffset())));
+ }
+
+ /**
+ * Moves all nodes up.
+ */
+ public void moveAllNodesUp() {
+ getComponent().getLocations().forEach(loc -> loc.setY(loc.getY() + NudgeDirection.UP.getYOffset()));
+ getComponent().getDisplayableEdges().forEach(edge -> edge.getNails().forEach(nail -> nail.setY(nail.getY() + NudgeDirection.UP.getYOffset())));
+ }
+
+ /***
+ * Handle the component being pressed based on the mouse button and hotkeys
+ * @param event to use for handling the action
+ */
@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());
+
if ((event.isShiftDown() && event.isPrimaryButtonDown()) || event.isMiddleButtonDown()) {
final Location location = new Location();
- location.initialize();
+ location.initialize(getComponent().getUniqueLocationId());
location.setX(event.getX());
location.setY(event.getY());
- location.setColorIntensity(getComponent().getColorIntensity());
location.setColor(getComponent().getColor());
if (unfinishedEdge != null) {
@@ -918,10 +844,10 @@ private void modelContainerPressed(final MouseEvent event) {
});
} else if (event.isSecondaryButtonDown()) {
if (unfinishedEdge == null) {
- contextMenu.show(JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX() * EcdarController.getActiveCanvasZoomFactor().get(), event.getY() * EcdarController.getActiveCanvasZoomFactor().get());
+ contextMenu.show(JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
} else {
initializeFinishEdgeContextMenu(unfinishedEdge);
- finishEdgeContextMenu.show(JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX() * EcdarController.getActiveCanvasZoomFactor().get(), event.getY() * EcdarController.getActiveCanvasZoomFactor().get());
+ finishEdgeContextMenu.show(JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
}
} else if (event.isPrimaryButtonDown()) {
// We are drawing an edge
@@ -950,12 +876,58 @@ private void modelContainerDragged() {
contextMenu.hide();
}
- public MouseTracker getMouseTracker() {
- return mouseTracker;
+ @Override
+ public HighLevelModel getModel() {
+ return getComponent();
}
+ /**
+ * 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
- public HighLevelModelObject getModel() {
- return getComponent();
+ double getDragAnchorMinWidth() {
+ double minWidth = 10 * Ecdar.CANVAS_PADDING;
+
+ for (final Location location : getComponent().getLocations()) {
+ minWidth = Math.max(minWidth, location.getX() + Ecdar.CANVAS_PADDING * 2);
+ }
+
+ for (final Edge edge : getComponent().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() {
+ double minHeight = 10 * Ecdar.CANVAS_PADDING;
+
+ for (final Location location : getComponent().getLocations()) {
+ minHeight = Math.max(minHeight, location.getY() + Ecdar.CANVAS_PADDING * 2);
+ }
+
+ for (final Edge edge : getComponent().getEdges()) {
+ for (final Nail nail : edge.getNails()) {
+ minHeight = Math.max(minHeight, nail.getY() + Ecdar.CANVAS_PADDING);
+ }
+ }
+
+ //Component should not get smaller than the height of the input/output signature containers
+ minHeight = Math.max(inputSignatureContainer.getHeight(), minHeight);
+ minHeight = Math.max(outputSignatureContainer.getHeight(), minHeight);
+
+ return minHeight;
}
}
diff --git a/src/main/java/ecdar/controllers/ComponentInstanceController.java b/src/main/java/ecdar/controllers/ComponentInstanceController.java
index a706eb95..547ffd78 100644
--- a/src/main/java/ecdar/controllers/ComponentInstanceController.java
+++ b/src/main/java/ecdar/controllers/ComponentInstanceController.java
@@ -96,7 +96,7 @@ private void initializeDropDownMenu(final EcdarSystem system) {
* Listens to an edge to update whether the root has an edge.
* @param edge the edge to update with
*/
- private void handleHasEdge(final EcdarSystemEdge edge) {
+ private void handleHasEdge(final SystemEdge edge) {
edge.getTempNodeProperty().addListener((observable -> updateHasEdge(edge)));
edge.getChildProperty().addListener((observable -> updateHasEdge(edge)));
edge.getParentProperty().addListener((observable -> updateHasEdge(edge)));
@@ -106,7 +106,7 @@ private void handleHasEdge(final EcdarSystemEdge edge) {
* Update has edge property to whether the instance is in a given edge.
* @param edge the given edge
*/
- private void updateHasEdge(final EcdarSystemEdge edge) {
+ private void updateHasEdge(final SystemEdge edge) {
hasEdge.set(edge.isInEdge(getInstance()));
}
@@ -114,7 +114,7 @@ private void updateHasEdge(final EcdarSystemEdge edge) {
private void onMouseClicked(final MouseEvent event) {
event.consume();
- final EcdarSystemEdge unfinishedEdge = getSystem().getUnfinishedEdge();
+ final SystemEdge unfinishedEdge = getSystem().getUnfinishedEdge();
if ((event.isShiftDown() && event.getButton().equals(MouseButton.PRIMARY)) || event.getButton().equals(MouseButton.MIDDLE)) {
// If shift click or middle click a component instance, create a new edge
@@ -147,11 +147,11 @@ private void onMouseClicked(final MouseEvent event) {
}
/***
- * Helper method to create a new EcdarSystemEdge and add it to the current system and component instance
- * @return The newly created EcdarSystemEdge
+ * Helper method to create a new SystemEdge and add it to the current system and component instance
+ * @return The newly created SystemEdge
*/
- private EcdarSystemEdge createNewSystemEdge() {
- final EcdarSystemEdge edge = new EcdarSystemEdge(getInstance());
+ private SystemEdge createNewSystemEdge() {
+ final SystemEdge edge = new SystemEdge(getInstance());
getSystem().addEdge(edge);
hasEdge.set(true);
diff --git a/src/main/java/ecdar/controllers/ComponentOperatorController.java b/src/main/java/ecdar/controllers/ComponentOperatorController.java
index 7da44f1c..f0abc60f 100644
--- a/src/main/java/ecdar/controllers/ComponentOperatorController.java
+++ b/src/main/java/ecdar/controllers/ComponentOperatorController.java
@@ -1,7 +1,7 @@
package ecdar.controllers;
import ecdar.abstractions.ComponentOperator;
import ecdar.abstractions.EcdarSystem;
-import ecdar.abstractions.EcdarSystemEdge;
+import ecdar.abstractions.SystemEdge;
import ecdar.presentations.DropDownMenu;
import ecdar.presentations.MenuElement;
import com.jfoenix.controls.JFXPopup;
@@ -83,7 +83,7 @@ private void showContextMenu(MouseEvent mouseEvent) {
contextMenu.addMenuElement(new MenuElement("Draw Edge")
.setClickable(() -> {
- final EcdarSystemEdge edge = new EcdarSystemEdge(operator);
+ final SystemEdge edge = new SystemEdge(operator);
getSystem().addEdge(edge);
contextMenu.hide();
@@ -104,7 +104,7 @@ private void showContextMenu(MouseEvent mouseEvent) {
* Listens to an edge to update whether the operator has a parent edge.
* @param parentEdge the edge to update with
*/
- private void updateHasParent(final EcdarSystemEdge parentEdge) {
+ private void updateHasParent(final SystemEdge parentEdge) {
// The operator has a parent iff the supposed parent edge has the operator as a child
parentEdge.getChildProperty().addListener(((observable, oldValue, newValue) -> hasParent.set(getOperator().equals(newValue))));
}
@@ -117,7 +117,7 @@ private void updateHasParent(final EcdarSystemEdge parentEdge) {
private void onMouseClicked(final MouseEvent event) {
event.consume();
- final EcdarSystemEdge unfinishedEdge = getSystem().getUnfinishedEdge();
+ final SystemEdge unfinishedEdge = getSystem().getUnfinishedEdge();
// if primary clicked and there is an unfinished edge, finish it with the system root as target
if (unfinishedEdge != null && event.getButton().equals(MouseButton.PRIMARY)) {
diff --git a/src/main/java/ecdar/controllers/DeclarationsController.java b/src/main/java/ecdar/controllers/DeclarationsController.java
index b4ab05e0..ee474f86 100644
--- a/src/main/java/ecdar/controllers/DeclarationsController.java
+++ b/src/main/java/ecdar/controllers/DeclarationsController.java
@@ -1,8 +1,8 @@
package ecdar.controllers;
-import ecdar.Ecdar;
import ecdar.abstractions.Declarations;
-import ecdar.presentations.ComponentPresentation;
+import ecdar.abstractions.HighLevelModel;
+import ecdar.utility.helpers.UPPAALSyntaxHighlighter;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.Initializable;
@@ -17,10 +17,10 @@
/**
* Controller for overall declarations.
*/
-public class DeclarationsController implements Initializable {
- public StyleClassedTextArea textArea;
+public class DeclarationsController extends HighLevelModelController implements Initializable {
public StackPane root;
public BorderPane frame;
+ public StyleClassedTextArea textArea;
private final ObjectProperty declarations;
@@ -34,21 +34,9 @@ public void setDeclarations(final Declarations declarations) {
@Override
public void initialize(final URL location, final ResourceBundle resources) {
- initializeWidthAndHeight();
initializeText();
}
- /**
- * Initializes width and height of the text editor field, such that it fills up the whole canvas
- */
- private void initializeWidthAndHeight() {
- // Fetch width and height of canvas and update
- root.minWidthProperty().bind(Ecdar.getPresentation().getController().getEditorPresentation().getController().canvasPane.minWidthProperty());
- root.maxWidthProperty().bind(Ecdar.getPresentation().getController().getEditorPresentation().getController().canvasPane.maxWidthProperty());
- root.minHeightProperty().bind(Ecdar.getPresentation().getController().getEditorPresentation().getController().canvasPane.minHeightProperty());
- root.maxHeightProperty().bind(Ecdar.getPresentation().getController().getEditorPresentation().getController().canvasPane.maxHeightProperty());
- }
-
/**
* Sets up the linenumbers and binds the text in the text area to the declaration object
*/
@@ -71,6 +59,11 @@ private void initializeText() {
* Updates highlighting of the text in the text area.
*/
public void updateHighlighting() {
- textArea.setStyleSpans(0, ComponentPresentation.computeHighlighting(declarations.get().getDeclarationsText()));
+ textArea.setStyleSpans(0, UPPAALSyntaxHighlighter.computeHighlighting(declarations.get().getDeclarationsText()));
+ }
+
+ @Override
+ public HighLevelModel getModel() {
+ return declarations.get();
}
}
diff --git a/src/main/java/ecdar/controllers/EcdarController.java b/src/main/java/ecdar/controllers/EcdarController.java
index 76f49469..0a135025 100644
--- a/src/main/java/ecdar/controllers/EcdarController.java
+++ b/src/main/java/ecdar/controllers/EcdarController.java
@@ -6,9 +6,9 @@
import ecdar.Ecdar;
import ecdar.abstractions.*;
import ecdar.backend.BackendHelper;
-import ecdar.backend.BackendInstance;
-import ecdar.backend.SimulationHandler;
+import ecdar.backend.Engine;
import ecdar.code_analysis.CodeAnalysis;
+import ecdar.mutation.MutationTestPlanPresentation;
import ecdar.mutation.models.MutationTestPlan;
import ecdar.presentations.*;
import ecdar.utility.UndoRedoStack;
@@ -48,7 +48,6 @@
import java.util.stream.Collectors;
public class EcdarController implements Initializable {
- private SimulationHandler simulationHandler;
// View stuff
public StackPane root;
public BorderPane borderPane;
@@ -59,8 +58,6 @@ public class EcdarController implements Initializable {
public StackPane modellingHelpDialogContainer;
public JFXDialog modellingHelpDialog;
public StackPane modalBar;
- public JFXTextField queryTextField;
- public JFXTextField commentTextField;
public ImageView helpInitialImage;
public StackPane helpInitialPane;
@@ -109,7 +106,7 @@ public class EcdarController implements Initializable {
public MenuItem menuBarFileExportAsPngNoBorder;
public MenuItem menuBarOptionsCache;
public MenuItem menuBarOptionsBackgroundQueries;
- public MenuItem menuBarOptionsBackendOptions;
+ public MenuItem menuBarOptionsEngineOptions;
public MenuItem menuBarHelpHelp;
public MenuItem menuBarHelpAbout;
public MenuItem menuBarHelpTest;
@@ -126,8 +123,8 @@ public class EcdarController implements Initializable {
public Text queryTextResult;
public Text queryTextQuery;
- public StackPane backendOptionsDialogContainer;
- public BackendOptionsDialogPresentation backendOptionsDialog;
+ public StackPane engineOptionsDialogContainer;
+ public EngineOptionsDialogPresentation engineOptionsDialog;
public StackPane simulationInitializationDialogContainer;
public SimulationInitializationDialogPresentation simulationInitializationDialog;
@@ -153,12 +150,7 @@ public enum Mode {
public static final ObjectProperty currentMode = new SimpleObjectProperty<>(Mode.Editor);
private static final EditorPresentation editorPresentation = new EditorPresentation();
- public final ProjectPanePresentation projectPane = new ProjectPanePresentation();
- public final QueryPanePresentation queryPane = new QueryPanePresentation();
-
private static final SimulatorPresentation simulatorPresentation = new SimulatorPresentation();
- public final LeftSimPanePresentation leftSimPane = new LeftSimPanePresentation();
- public final RightSimPanePresentation rightSimPane = new RightSimPanePresentation();
@Override
public void initialize(final URL location, final ResourceBundle resources) {
@@ -167,18 +159,7 @@ public void initialize(final URL location, final ResourceBundle resources) {
initializeStatusBar();
initializeMenuBar();
- // Update file coloring when active model changes
- editorPresentation.getController().getActiveCanvasPresentation().getController().activeComponentProperty().addListener(observable -> projectPane.getController().updateColorsOnFilePresentations());
- editorPresentation.getController().activeCanvasPresentationProperty().addListener(observable -> projectPane.getController().updateColorsOnFilePresentations());
-
- leftSimPane.minWidthProperty().bind(projectPane.minWidthProperty());
- leftSimPane.maxWidthProperty().bind(projectPane.maxWidthProperty());
- rightSimPane.minWidthProperty().bind(queryPane.minWidthProperty());
- rightSimPane.maxWidthProperty().bind(queryPane.maxWidthProperty());
-
enterEditorMode();
-
- simulationHandler = Ecdar.getSimulationHandler();
}
public StackPane getCenter() {
@@ -201,22 +182,20 @@ public SimulatorPresentation getSimulatorPresentation() {
return simulatorPresentation;
}
- public static CanvasPresentation getActiveCanvasPresentation() {
- return editorPresentation.getController().getActiveCanvasPresentation();
- }
-
- public static DoubleProperty getActiveCanvasZoomFactor() {
- return getActiveCanvasPresentation().getController().zoomHelper.currentZoomFactor;
- }
-
- public static void setActiveCanvasPresentation(CanvasPresentation newActiveCanvasPresentation) {
- getActiveCanvasPresentation().setOpacity(0.75);
- newActiveCanvasPresentation.setOpacity(1);
- editorPresentation.getController().setActiveCanvasPresentation(newActiveCanvasPresentation);
+ public StackPane getLeftModePane() {
+ if (currentMode.get().equals(Mode.Editor)) {
+ return editorPresentation.getController().getLeftPane();
+ } else {
+ return simulatorPresentation.getController().getLeftPane();
+ }
}
- public static void setActiveModelForActiveCanvas(HighLevelModelObject newActiveModel) {
- getActiveCanvasPresentation().getController().setActiveModel(newActiveModel);
+ public StackPane getRightModePane() {
+ if (currentMode.get().equals(Mode.Editor)) {
+ return editorPresentation.getController().getRightPane();
+ } else {
+ return simulatorPresentation.getController().getRightPane();
+ }
}
public static void setTemporaryComponentWatermarkVisibility(boolean visibility) {
@@ -265,7 +244,29 @@ private void initializeDialogs() {
_queryTextQuery = queryTextQuery;
initializeDialog(queryDialog, queryDialogContainer);
- initializeBackendOptionsDialog();
+ initializeDialog(engineOptionsDialog, engineOptionsDialogContainer);
+
+ engineOptionsDialog.getController().resetEnginesButton.setOnMouseClicked(event -> {
+ engineOptionsDialog.getController().resetEnginesToDefault();
+ });
+
+ engineOptionsDialog.getController().closeButton.setOnMouseClicked(event -> {
+ engineOptionsDialog.getController().cancelEngineOptionsChanges();
+ engineOptionsDialog.close();
+ });
+
+ engineOptionsDialog.getController().saveButton.setOnMouseClicked(event -> {
+ if (engineOptionsDialog.getController().saveChangesToEngineOptions()) {
+ engineOptionsDialog.close();
+ }
+ });
+
+ if (BackendHelper.getEngines().size() < 1) {
+ Ecdar.showToast("No engines were found. Download j-Ecdar or Reveaal, or add another engine to fix this. No queries can be executed without engines.");
+ } else {
+ Engine defaultBackend = BackendHelper.getEngines().stream().filter(Engine::isDefault).findFirst().orElse(BackendHelper.getEngines().get(0));
+ BackendHelper.setDefaultEngine(defaultBackend);
+ }
initializeDialog(simulationInitializationDialog, simulationInitializationDialogContainer);
@@ -277,37 +278,12 @@ private void initializeDialogs() {
simulationInitializationDialog.getController().startButton.setOnMouseClicked(event -> {
// ToDo NIELS: Start simulation of selected query
- simulationHandler.setComposition(simulationInitializationDialog.getController().simulationComboBox.getSelectionModel().getSelectedItem());
+ SimulatorController.simulationHandler.setComposition(simulationInitializationDialog.getController().simulationComboBox.getSelectionModel().getSelectedItem());
currentMode.setValue(Mode.Simulator);
simulationInitializationDialog.close();
});
}
- private void initializeBackendOptionsDialog() {
- initializeDialog(backendOptionsDialog, backendOptionsDialogContainer);
- backendOptionsDialog.getController().resetBackendsButton.setOnMouseClicked(event -> {
- backendOptionsDialog.getController().resetBackendsToDefault();
- });
-
- backendOptionsDialog.getController().closeButton.setOnMouseClicked(event -> {
- backendOptionsDialog.getController().cancelBackendOptionsChanges();
- backendOptionsDialog.close();
- });
-
- backendOptionsDialog.getController().saveButton.setOnMouseClicked(event -> {
- if (backendOptionsDialog.getController().saveChangesToBackendOptions()) {
- backendOptionsDialog.close();
- }
- });
-
- if (BackendHelper.getBackendInstances().size() < 1) {
- Ecdar.showToast("No engines were found. Download j-Ecdar or Reveaal, or add another engine to fix this. No queries can be executed without engines.");
- } else {
- BackendInstance defaultBackend = BackendHelper.getBackendInstances().stream().filter(BackendInstance::isDefault).findFirst().orElse(BackendHelper.getBackendInstances().get(0));
- BackendHelper.setDefaultBackendInstance(defaultBackend);
- }
- }
-
private void initializeDialog(JFXDialog dialog, StackPane dialogContainer) {
dialog.setDialogContainer(dialogContainer);
dialogContainer.opacityProperty().bind(dialog.getChildren().get(0).scaleXProperty());
@@ -321,9 +297,6 @@ private void initializeDialog(JFXDialog dialog, StackPane dialogContainer) {
dialogContainer.setMouseTransparent(false);
});
- projectPane.getStyleClass().add("responsive-pane-sizing");
- queryPane.getStyleClass().add("responsive-pane-sizing");
-
initializeKeybindings();
initializeStatusBar();
}
@@ -349,19 +322,6 @@ private void intitializeTemporaryComponentWatermark() {
* - Colors
*/
private void initializeKeybindings() {
- //Press ctrl+N or cmd+N to create a new component. The canvas changes to this new component
- KeyCodeCombination combination = new KeyCodeCombination(KeyCode.N, KeyCombination.SHORTCUT_DOWN);
- Keybind binding = new Keybind(combination, (event) -> {
- final Component newComponent = new Component(true);
- UndoRedoStack.pushAndPerform(() -> { // Perform
- Ecdar.getProject().getComponents().add(newComponent);
- setActiveModelForActiveCanvas(newComponent);
- }, () -> { // Undo
- Ecdar.getProject().getComponents().remove(newComponent);
- }, "Created new component: " + newComponent.getName(), "add-circle");
- });
- KeyboardTracker.registerKeybind(KeyboardTracker.CREATE_COMPONENT, binding);
-
// Keybind for nudging the selected elements
KeyboardTracker.registerKeybind(KeyboardTracker.NUDGE_UP, new Keybind(new KeyCodeCombination(KeyCode.UP), (event) -> {
event.consume();
@@ -469,10 +429,10 @@ private void initializeOptionsMenu() {
menuBarOptionsCache.getGraphic().opacityProperty().bind(new When(isCached).then(1).otherwise(0));
});
- menuBarOptionsBackendOptions.setOnAction(event -> {
- backendOptionsDialogContainer.setVisible(true);
- backendOptionsDialog.show(backendOptionsDialogContainer);
- backendOptionsDialog.setMouseTransparent(false);
+ menuBarOptionsEngineOptions.setOnAction(event -> {
+ engineOptionsDialogContainer.setVisible(true);
+ engineOptionsDialog.show(engineOptionsDialogContainer);
+ engineOptionsDialog.setMouseTransparent(false);
});
menuBarOptionsBackgroundQueries.setOnAction(event -> {
@@ -487,33 +447,33 @@ private void initializeOptionsMenu() {
private void initializeEditMenu() {
menuEditMoveLeft.setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCombination.CONTROL_DOWN));
menuEditMoveLeft.setOnAction(event -> {
- final HighLevelModelObject activeModel = getActiveCanvasPresentation().getController().getActiveModel();
-
- if (activeModel instanceof Component) ((Component) activeModel).moveAllNodesLeft();
+ 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.");
});
menuEditMoveRight.setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCombination.CONTROL_DOWN));
menuEditMoveRight.setOnAction(event -> {
- final HighLevelModelObject activeModel = getActiveCanvasPresentation().getController().getActiveModel();
-
- if (activeModel instanceof Component) ((Component) activeModel).moveAllNodesRight();
+ 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.");
});
menuEditMoveUp.setAccelerator(new KeyCodeCombination(KeyCode.UP, KeyCombination.CONTROL_DOWN));
menuEditMoveUp.setOnAction(event -> {
- final HighLevelModelObject activeModel = getActiveCanvasPresentation().getController().getActiveModel();
-
- if (activeModel instanceof Component) ((Component) activeModel).moveAllNodesUp();
+ 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.");
});
menuEditMoveDown.setAccelerator(new KeyCodeCombination(KeyCode.DOWN, KeyCombination.CONTROL_DOWN));
menuEditMoveDown.setOnAction(event -> {
- final HighLevelModelObject activeModel = getActiveCanvasPresentation().getController().getActiveModel();
-
- if (activeModel instanceof Component) ((Component) activeModel).moveAllNodesDown();
+ 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.");
});
}
@@ -550,13 +510,14 @@ private void initializeViewMenu() {
menuBarViewCanvasSplit.getGraphic().setOpacity(1);
menuBarViewCanvasSplit.setOnAction(event -> {
- final BooleanProperty isSplit = Ecdar.toggleCanvasSplit();
- if (isSplit.get()) {
- Platform.runLater(() -> editorPresentation.getController().setCanvasModeToSingular());
- menuBarViewCanvasSplit.setText("Split canvas");
+ Ecdar.toggleCanvasSplit();
+ });
+
+ Ecdar.isSplitProperty().addListener((observable, oldValue, newValue) -> {
+ if (newValue) {
+ Platform.runLater(editorPresentation.getController()::setCanvasModeToSplit);
} else {
- Platform.runLater(() -> editorPresentation.getController().setCanvasModeToSplit());
- menuBarViewCanvasSplit.setText("Merge canvases");
+ Platform.runLater(editorPresentation.getController()::setCanvasModeToSingular);
}
});
@@ -568,7 +529,7 @@ private void initializeViewMenu() {
return;
}
- if (!simulationHandler.isSimulationRunning()) {
+ if (!SimulatorController.getSimulationHandler().isSimulationRunning()) {
ArrayList queryOptions = Ecdar.getProject().getQueries().stream().map(Query::getQuery).collect(Collectors.toCollection(ArrayList::new));
if (!simulationInitializationDialog.getController().simulationComboBox.getItems().equals(queryOptions)) {
simulationInitializationDialog.getController().simulationComboBox.getItems().setAll(queryOptions);
@@ -623,7 +584,7 @@ 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);
@@ -655,8 +616,7 @@ private void initializeOpenProjectMenuItem() {
final File file = projectPicker.showDialog(root.getScene().getWindow());
if (file != null) {
try {
- Ecdar.projectDirectory.set(file.getAbsolutePath());
- Ecdar.initializeProjectFolder();
+ setProjectDirectory(file.getAbsolutePath());
UndoRedoStack.clear();
addProjectToRecentProjects(file.getAbsolutePath());
} catch (final IOException e) {
@@ -676,8 +636,7 @@ private void initializeRecentProjectsMenu() {
item.setOnAction(event -> {
try {
- Ecdar.projectDirectory.set(path);
- Ecdar.initializeProjectFolder();
+ setProjectDirectory(path);
} catch (IOException ex) {
Ecdar.showToast("Unable to load project: \"" + path + "\"");
}
@@ -703,9 +662,15 @@ private void initializeRecentProjectsMenu() {
menuBarFileRecentProjects.getItems().add(item);
}
+ private static void setProjectDirectory(String path) throws IOException {
+ Ecdar.projectDirectory.set(path);
+ Ecdar.initializeProjectFolder();
+ Ecdar.isSplitProperty().set(false);
+ }
+
/**
* 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() {
@@ -818,7 +783,7 @@ private void initializeNewMutationTestObjectMenuItem() {
UndoRedoStack.pushAndPerform(() -> { // Perform
Ecdar.getProject().getTestPlans().add(newPlan);
- setActiveModelForActiveCanvas(newPlan);
+ editorPresentation.getController().setActiveModelPresentationForActiveCanvas(new MutationTestPlanPresentation(newPlan));
}, () -> { // Undo
Ecdar.getProject().getTestPlans().remove(newPlan);
}, "Created new mutation test plan", "");
@@ -828,15 +793,18 @@ private void initializeNewMutationTestObjectMenuItem() {
/**
* Creates a new project.
*/
- private static void createNewProject() {
+ private void createNewProject() {
CodeAnalysis.disable();
CodeAnalysis.clearErrorsAndWarnings();
- Ecdar.projectDirectory.set(null);
+ try {
+ setProjectDirectory(null);
+ } catch (IOException ex) {
+ Ecdar.showToast("Unable to initialize new project directory");
+ }
- Ecdar.getProject().reset();
- setActiveModelForActiveCanvas(Ecdar.getProject().getComponents().get(0));
+ editorPresentation.getController().setActiveModelPresentationForActiveCanvas(editorPresentation.getController().projectPane.getController().getComponentPresentations().get(0));
UndoRedoStack.clear();
@@ -850,12 +818,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().getActiveModel() instanceof Component || getActiveCanvasPresentation().getController().getActiveModel() instanceof EcdarSystem)) {
+ 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();
@@ -874,7 +842,7 @@ private void initializeFileExportAsPng() {
});
menuBarFileExportAsPngNoBorder.setOnAction(event -> {
- CanvasPresentation canvas = getActiveCanvasPresentation();
+ CanvasPresentation canvas = editorPresentation.getController().getActiveCanvasPresentation();
final ComponentPresentation presentation = canvas.getController().getActiveComponentPresentation();
@@ -898,10 +866,7 @@ private void initializeFileExportAsPng() {
*/
private void enterEditorMode() {
borderPane.setCenter(editorPresentation);
- leftPane.getChildren().clear();
- leftPane.getChildren().add(projectPane);
- rightPane.getChildren().clear();
- rightPane.getChildren().add(queryPane);
+ updateSidePanes(editorPresentation.getController());
}
/**
@@ -910,12 +875,17 @@ private void enterEditorMode() {
*/
private void enterSimulatorMode() {
simulatorPresentation.getController().willShow();
-
borderPane.setCenter(simulatorPresentation);
+ updateSidePanes(simulatorPresentation.getController());
+ }
+
+ private void updateSidePanes(ModeController modeController) {
leftPane.getChildren().clear();
- leftPane.getChildren().add(leftSimPane);
+ leftPane.getChildren().add(modeController.getLeftPane());
rightPane.getChildren().clear();
- rightPane.getChildren().add(rightSimPane);
+ rightPane.getChildren().add(modeController.getRightPane());
+
+
}
/**
@@ -953,7 +923,7 @@ private WritableImage scaleAndTakeSnapshot(CanvasPresentation canvas) {
* @param image the image
*/
private void CropAndExportImage(final WritableImage image) {
- final String name = getActiveCanvasPresentation().getController().getActiveModel().getName();
+ final String name = editorPresentation.getController().getActiveCanvasPresentation().getController().getActiveModelPresentation().getController().getModel().getName();
final FileChooser filePicker = new FileChooser();
filePicker.setTitle("Export png");
@@ -1075,7 +1045,6 @@ private static int getAutoCropRightX(final BufferedImage image) {
throw new IllegalArgumentException("Image is all white");
}
-
private void nudgeSelected(final NudgeDirection direction) {
final List selectedElements = SelectHelper.getSelectedElements();
diff --git a/src/main/java/ecdar/controllers/EdgeController.java b/src/main/java/ecdar/controllers/EdgeController.java
index 42bde140..3c65cb0d 100644
--- a/src/main/java/ecdar/controllers/EdgeController.java
+++ b/src/main/java/ecdar/controllers/EdgeController.java
@@ -8,6 +8,7 @@
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;
import ecdar.utility.keyboard.KeyboardTracker;
@@ -442,11 +443,11 @@ public void onChanged(final Change extends Link> 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);
@@ -593,8 +594,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);
}
@@ -722,24 +723,18 @@ public void edgeDragged(final MouseEvent event) {
}
@Override
- public void color(final Color color, final Color.Intensity intensity) {
+ public void color(final EnabledColor color) {
final DisplayableEdge edge = getEdge();
// Set the color of the edge
- edge.setColorIntensity(intensity);
edge.setColor(color);
}
@Override
- public Color getColor() {
+ public EnabledColor getColor() {
return getEdge().getColor();
}
- @Override
- public Color.Intensity getColorIntensity() {
- return getEdge().getColorIntensity();
- }
-
@Override
public ItemDragHelper.DragBounds getDragBounds() {
return ItemDragHelper.DragBounds.generateLooseDragBounds();
diff --git a/src/main/java/ecdar/controllers/EditorController.java b/src/main/java/ecdar/controllers/EditorController.java
index 15557dea..cf12107d 100644
--- a/src/main/java/ecdar/controllers/EditorController.java
+++ b/src/main/java/ecdar/controllers/EditorController.java
@@ -7,8 +7,7 @@
import ecdar.abstractions.Component;
import ecdar.abstractions.DisplayableEdge;
import ecdar.abstractions.EdgeStatus;
-import ecdar.abstractions.HighLevelModelObject;
-import ecdar.presentations.CanvasPresentation;
+import ecdar.presentations.*;
import ecdar.utility.UndoRedoStack;
import ecdar.utility.colors.Color;
import ecdar.utility.colors.EnabledColor;
@@ -16,7 +15,9 @@
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;
@@ -30,10 +31,11 @@
import java.net.URL;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.ResourceBundle;
-public class EditorController implements Initializable {
+public class EditorController implements ModeController, Initializable {
public VBox root;
public HBox toolbar;
public JFXRippler colorSelected;
@@ -45,6 +47,9 @@ public class EditorController implements Initializable {
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());
@@ -53,6 +58,35 @@ 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() {
@@ -73,11 +107,15 @@ public void setActiveCanvasPresentation(CanvasPresentation newActiveCanvasPresen
activeCanvasPresentation.set(newActiveCanvasPresentation);
}
- public void setActiveModelForActiveCanvas(HighLevelModelObject newActiveModel) {
- EcdarController.setActiveModelForActiveCanvas(newActiveModel);
+ public void setActiveModelPresentationForActiveCanvas(HighLevelModelPresentation newActiveModelPresentation) {
+ getActiveCanvasPresentation().getController().setActiveModelPresentation(newActiveModelPresentation);
// Change zoom level to fit new active model
- Platform.runLater(() -> EcdarController.getActiveCanvasPresentation().getController().zoomHelper.zoomToFit());
+ Platform.runLater(() -> getActiveCanvasPresentation().getController().zoomHelper.zoomToFit());
+ }
+
+ public DoubleProperty getActiveCanvasZoomFactor() {
+ return getActiveCanvasPresentation().getController().zoomHelper.currentZoomFactor;
}
private void initializeCanvasPane() {
@@ -92,7 +130,7 @@ private void initializeKeybindings() {
final List> previousColor = new ArrayList<>();
SelectHelper.getSelectedElements().forEach(selectable -> {
- previousColor.add(new Pair<>(selectable, new EnabledColor(selectable.getColor(), selectable.getColorIntensity())));
+ previousColor.add(new Pair<>(selectable, selectable.getColor()));
});
changeColorOnSelectedElements(enabledColor, previousColor);
SelectHelper.clearSelectedElements();
@@ -159,9 +197,9 @@ public void changeColorOnSelectedElements(final EnabledColor enabledColor,
final List> previousColor) {
UndoRedoStack.pushAndPerform(() -> { // Perform
SelectHelper.getSelectedElements()
- .forEach(selectable -> selectable.color(enabledColor.color, enabledColor.intensity));
+ .forEach(selectable -> selectable.color(enabledColor));
}, () -> { // Undo
- previousColor.forEach(selectableEnabledColorPair -> selectableEnabledColorPair.getKey().color(selectableEnabledColorPair.getValue().color, selectableEnabledColorPair.getValue().intensity));
+ 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");
}
@@ -171,28 +209,26 @@ public void changeColorOnSelectedElements(final EnabledColor enabledColor,
void setCanvasModeToSingular() {
canvasPane.getChildren().clear();
CanvasPresentation canvasPresentation = new CanvasPresentation();
- HighLevelModelObject model = activeCanvasPresentation.get().getController().getActiveModel();
- if (model != null) {
- canvasPresentation.getController().setActiveModel(activeCanvasPresentation.get().getController().getActiveModel());
- } else {
- // If no components where found, the project has not been initialized. The active model will be updated when the project is initialized
- canvasPresentation.getController().setActiveModel(Ecdar.getProject().getComponents().stream().findFirst().orElse(null));
+ if (activeCanvasPresentation.get().getController().getActiveModelPresentation() != null) {
+ canvasPresentation.getController().setActiveModelPresentation(activeCanvasPresentation.get().getController().getActiveModelPresentation());
}
canvasPane.getChildren().add(canvasPresentation);
- activeCanvasPresentation.set(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())));
}
/**
@@ -220,12 +256,12 @@ void setCanvasModeToSplit() {
canvasGrid.getRowConstraints().add(row1);
canvasGrid.getRowConstraints().add(row1);
- ObservableList components = Ecdar.getProject().getComponents();
+ ObservableList components = projectPane.getController().getComponentPresentations();
int currentCompNum = 0, numComponents = components.size();
// Add the canvasPresentation at the top-left
CanvasPresentation canvasPresentation = initializeNewCanvasPresentation();
- canvasPresentation.getController().setActiveModel(getActiveCanvasPresentation().getController().getActiveModel());
+ canvasPresentation.getController().setActiveModelPresentation(getActiveCanvasPresentation().getController().getActiveModelPresentation());
canvasGrid.add(canvasPresentation, 0, 0);
setActiveCanvasPresentation(canvasPresentation);
@@ -236,7 +272,7 @@ void setCanvasModeToSplit() {
// Update the startIndex for the next canvasPresentation
for (int i = 0; i < numComponents; i++) {
- if (canvasPresentation.getController().getActiveModel() != null && canvasPresentation.getController().getActiveModel().equals(components.get(i))) {
+ if (canvasPresentation.getController().getActiveModelPresentation() != null && canvasPresentation.getController().getActiveModelPresentation().getController().getModel().equals(components.get(i))) {
currentCompNum = i + 1;
}
}
@@ -248,7 +284,7 @@ void setCanvasModeToSplit() {
// Update the startIndex for the next canvasPresentation
for (int i = 0; i < numComponents; i++)
- if (canvasPresentation.getController().getActiveModel() != null && canvasPresentation.getController().getActiveModel().equals(components.get(i))) {
+ if (canvasPresentation.getController().getActiveModelPresentation() != null && canvasPresentation.getController().getActiveModelPresentation().getController().getModel().equals(components.get(i))) {
currentCompNum = i + 1;
}
@@ -263,18 +299,18 @@ void setCanvasModeToSplit() {
/**
* 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 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 components, int startIndex) {
+ private CanvasPresentation initializeNewCanvasPresentationWithActiveComponent(ObservableList componentPresentations, int startIndex) {
CanvasPresentation canvasPresentation = initializeNewCanvasPresentation();
- int numComponents = components.size();
- canvasPresentation.getController().setActiveModel(null);
+ int numComponents = componentPresentations.size();
+ canvasPresentation.getController().setActiveModelPresentation(null);
for (int currentCompNum = startIndex; currentCompNum < numComponents; currentCompNum++) {
- if (getActiveCanvasPresentation().getController().getActiveModel() != components.get(currentCompNum)) {
- canvasPresentation.getController().setActiveModel(components.get(currentCompNum));
+ if (getActiveCanvasPresentation().getController().getActiveModelPresentation() != componentPresentations.get(currentCompNum)) {
+ canvasPresentation.getController().setActiveModelPresentation(componentPresentations.get(currentCompNum));
break;
}
}
@@ -377,4 +413,14 @@ private void deleteSelectedClicked() {
SelectHelper.clearSelectedElements();
}
+
+ @Override
+ public StackPane getLeftPane() {
+ return projectPane;
+ }
+
+ @Override
+ public StackPane getRightPane() {
+ return queryPane;
+ }
}
diff --git a/src/main/java/ecdar/controllers/BackendInstanceController.java b/src/main/java/ecdar/controllers/EngineInstanceController.java
similarity index 52%
rename from src/main/java/ecdar/controllers/BackendInstanceController.java
rename to src/main/java/ecdar/controllers/EngineInstanceController.java
index c4f5337b..8daf2668 100644
--- a/src/main/java/ecdar/controllers/BackendInstanceController.java
+++ b/src/main/java/ecdar/controllers/EngineInstanceController.java
@@ -3,7 +3,7 @@
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXRippler;
import com.jfoenix.controls.JFXTextField;
-import ecdar.backend.BackendInstance;
+import ecdar.backend.Engine;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.fxml.FXML;
@@ -22,22 +22,22 @@
import java.net.URL;
import java.util.ResourceBundle;
-public class BackendInstanceController implements Initializable {
- private BackendInstance backendInstance = new BackendInstance();
+public class EngineInstanceController implements Initializable {
+ private Engine engine = new Engine();
/* Design elements */
- public Label backendNameIssue;
- public JFXRippler removeBackendRippler;
- public FontIcon removeBackendIcon;
+ public Label engineNameIssue;
+ public JFXRippler removeEngineRippler;
+ public FontIcon removeEngineIcon;
public FontIcon expansionIcon;
public StackPane content;
public JFXCheckBox isLocal;
public HBox addressSection;
- public HBox pathToBackendSection;
- public JFXRippler pickPathToBackend;
- public FontIcon pickPathToBackendIcon;
- public StackPane moveBackendInstanceUpRippler;
- public StackPane moveBackendInstanceDownRippler;
+ public HBox pathToEngineSection;
+ public JFXRippler pickPathToEngine;
+ public FontIcon pickPathToEngineIcon;
+ public StackPane moveEngineInstanceUpRippler;
+ public StackPane moveEngineInstanceDownRippler;
// Labels for showing potential issues
public Label locationIssue;
@@ -46,25 +46,24 @@ public class BackendInstanceController implements Initializable {
public Label portRangeIssue;
/* Input fields */
- public JFXTextField backendName;
+ public JFXTextField engineName;
public JFXTextField address;
- public JFXTextField pathToBackend;
+ public JFXTextField pathToEngine;
public JFXTextField portRangeStart;
public JFXTextField portRangeEnd;
- public RadioButton defaultBackendRadioButton;
- public JFXCheckBox threadSafeBackendCheckBox;
+ public RadioButton defaultEngineRadioButton;
+ public JFXCheckBox threadSafeEngineCheckBox;
@Override
public void initialize(URL location, ResourceBundle resources) {
Platform.runLater(() -> {
this.handleLocalPropertyChanged();
- moveBackendInstanceUpRippler.setCursor(Cursor.HAND);
- moveBackendInstanceDownRippler.setCursor(Cursor.HAND);
+ moveEngineInstanceUpRippler.setCursor(Cursor.HAND);
+ moveEngineInstanceDownRippler.setCursor(Cursor.HAND);
setHGrow();
- colorIconAsDisabledBasedOnProperty(removeBackendIcon, defaultBackendRadioButton.selectedProperty());
- colorIconAsDisabledBasedOnProperty(removeBackendIcon, threadSafeBackendCheckBox.selectedProperty());
- colorIconAsDisabledBasedOnProperty(pickPathToBackendIcon, backendInstance.getLockedProperty());
+ colorIconAsDisabledBasedOnProperty(removeEngineIcon, defaultEngineRadioButton.selectedProperty());
+ colorIconAsDisabledBasedOnProperty(pickPathToEngineIcon, engine.getLockedProperty());
});
}
@@ -74,7 +73,7 @@ public void initialize(URL location, ResourceBundle resources) {
* @param property The property to bind to
*/
private void colorIconAsDisabledBasedOnProperty(FontIcon icon, BooleanProperty property) {
- // Disallow the user to pick new backend file location for locked backends
+ // Disallow the user to pick new engine file location for locked engines
property.addListener((observable, oldValue, newValue) -> {
if (newValue) {
icon.setFill(Color.GREY);
@@ -86,22 +85,22 @@ private void colorIconAsDisabledBasedOnProperty(FontIcon icon, BooleanProperty p
}
/***
- * Sets the backend instance and overrides the current values of the input fields in the GUI.
- * @param instance the new BackendInstance
+ * Sets the engine instance and overrides the current values of the input fields in the GUI.
+ * @param instance the new Engine
*/
- public void setBackendInstance(BackendInstance instance) {
- this.backendInstance = instance;
+ public void setEngine(Engine instance) {
+ this.engine = instance;
- this.backendName.setText(instance.getName());
+ this.engineName.setText(instance.getName());
this.isLocal.setSelected(instance.isLocal());
- this.defaultBackendRadioButton.setSelected(instance.isDefault());
- this.threadSafeBackendCheckBox.setSelected(instance.isThreadSafe());
+ this.defaultEngineRadioButton.setSelected(instance.isDefault());
+ this.threadSafeEngineCheckBox.setSelected(instance.isThreadSafe());
// Check if the path or the address should be used
if (isLocal.isSelected()) {
- this.pathToBackend.setText(instance.getBackendLocation());
+ this.pathToEngine.setText(instance.getEngineLocation());
} else {
- this.address.setText(instance.getBackendLocation());
+ this.address.setText(instance.getIpAddress());
}
this.portRangeStart.setText(String.valueOf(instance.getPortStart()));
@@ -109,30 +108,30 @@ public void setBackendInstance(BackendInstance instance) {
}
/**
- * Updates the values of the backend instance to the values from the input fields.
- * @return The updated backend instance
+ * Updates the values of the engine instance to the values from the input fields.
+ * @return The updated engine instance
*/
- public BackendInstance updateBackendInstance() {
- backendInstance.setName(backendName.getText());
- backendInstance.setLocal(isLocal.isSelected());
- backendInstance.setDefault(defaultBackendRadioButton.isSelected());
- backendInstance.setIsThreadSafe(threadSafeBackendCheckBox.isSelected());
- backendInstance.setBackendLocation(isLocal.isSelected() ? pathToBackend.getText() : address.getText());
- backendInstance.setPortStart(Integer.parseInt(portRangeStart.getText()));
- backendInstance.setPortEnd(Integer.parseInt(portRangeEnd.getText()));
-
- return backendInstance;
+ 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()));
+
+ return engine;
}
private void setHGrow() {
- HBox.setHgrow(backendName.getParent().getParent().getParent(), Priority.ALWAYS);
- HBox.setHgrow(backendName.getParent(), Priority.ALWAYS);
- HBox.setHgrow(backendName, Priority.ALWAYS);
+ HBox.setHgrow(engineName.getParent().getParent().getParent(), Priority.ALWAYS);
+ HBox.setHgrow(engineName.getParent(), Priority.ALWAYS);
+ HBox.setHgrow(engineName, Priority.ALWAYS);
HBox.setHgrow(content, Priority.ALWAYS);
HBox.setHgrow(addressSection, Priority.ALWAYS);
HBox.setHgrow(address, Priority.ALWAYS);
- HBox.setHgrow(pathToBackendSection, Priority.ALWAYS);
- HBox.setHgrow(pathToBackend, Priority.ALWAYS);
+ HBox.setHgrow(pathToEngineSection, Priority.ALWAYS);
+ HBox.setHgrow(pathToEngine, Priority.ALWAYS);
HBox.setHgrow(portRangeStart, Priority.ALWAYS);
HBox.setHgrow(portRangeEnd, Priority.ALWAYS);
}
@@ -142,14 +141,14 @@ private void handleLocalPropertyChanged() {
address.setDisable(true);
addressSection.setVisible(false);
addressSection.setManaged(false);
- pathToBackendSection.setVisible(true);
- pathToBackendSection.setManaged(true);
+ pathToEngineSection.setVisible(true);
+ pathToEngineSection.setManaged(true);
} else {
address.setDisable(false);
addressSection.setVisible(true);
addressSection.setManaged(true);
- pathToBackendSection.setVisible(false);
- pathToBackendSection.setManaged(false);
+ pathToEngineSection.setVisible(false);
+ pathToEngineSection.setManaged(false);
}
}
@@ -172,23 +171,23 @@ private void expansionClicked() {
}
@FXML
- private void openPathToBackendDialog() {
+ private void openPathToEngineDialog() {
// Dialog title
- final FileChooser backendPicker = new FileChooser();
- backendPicker.setTitle("Choose backend");
+ final FileChooser enginePicker = new FileChooser();
+ enginePicker.setTitle("Choose Engine");
// The initial location for the file choosing dialog
- final File jarDir = new File(pathToBackend.getText()).getAbsoluteFile().getParentFile();
+ final File jarDir = new File(pathToEngine.getText()).getAbsoluteFile().getParentFile();
// If the file does not exist, we must be running it from a development environment, use a default location
if(jarDir.exists()) {
- backendPicker.setInitialDirectory(jarDir);
+ enginePicker.setInitialDirectory(jarDir);
}
// Prompt the user to find a file (will halt the UI thread)
- final File file = backendPicker.showOpenDialog(null);
+ final File file = enginePicker.showOpenDialog(null);
if(file != null) {
- pathToBackend.setText(file.getAbsolutePath());
+ pathToEngine.setText(file.getAbsolutePath());
}
}
}
diff --git a/src/main/java/ecdar/controllers/EngineOptionsDialogController.java b/src/main/java/ecdar/controllers/EngineOptionsDialogController.java
new file mode 100644
index 00000000..5f078ad3
--- /dev/null
+++ b/src/main/java/ecdar/controllers/EngineOptionsDialogController.java
@@ -0,0 +1,494 @@
+package ecdar.controllers;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonParser;
+import com.jfoenix.controls.JFXButton;
+import com.jfoenix.controls.JFXRippler;
+import ecdar.Ecdar;
+import ecdar.backend.Engine;
+import ecdar.backend.BackendException;
+import ecdar.backend.BackendHelper;
+import ecdar.presentations.EnginePresentation;
+import javafx.fxml.Initializable;
+import javafx.scene.Node;
+import javafx.scene.control.ToggleGroup;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import org.apache.commons.lang3.Range;
+import org.apache.commons.lang3.SystemUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+
+public class EngineOptionsDialogController implements Initializable {
+ public VBox engineInstanceList;
+ public JFXRippler addEngineButton;
+ public JFXButton closeButton;
+ public ToggleGroup defaultEngineToggleGroup = new ToggleGroup();
+ public JFXButton saveButton;
+ public JFXButton resetEnginesButton;
+
+ @Override
+ public void initialize(URL location, ResourceBundle resources) {
+ initializeEngineInstanceList();
+ }
+
+ /**
+ * Reverts any changes made to the engine options by reloading the options specified in the preference file,
+ * or to the default, if no engines are present in the preferences file.
+ */
+ public void cancelEngineOptionsChanges() {
+ initializeEngineInstanceList();
+ }
+
+ /**
+ * Saves the changes made to the engine options to the preferences file and returns true
+ * if no errors where found in the engine instance definitions, otherwise false.
+ *
+ * @return whether the changes could be saved,
+ * meaning that no errors where found in the changes made to the engine options
+ */
+ public boolean saveChangesToEngineOptions() {
+ if (this.engineInstanceListIsErrorFree()) {
+ ArrayList engines = new ArrayList<>();
+ for (Node engine : engineInstanceList.getChildren()) {
+ if (engine instanceof EnginePresentation) {
+ engines.add(((EnginePresentation) engine).getController().updateEngineInstance());
+ }
+ }
+
+ if (engines.size() < 1) {
+ Ecdar.showToast("Please add an engine instance or press: \"" + resetEnginesButton.getText() + "\"");
+ return false;
+ }
+
+ // Close all engine connections to avoid dangling engine connections when port range is changed
+ try {
+ BackendHelper.clearEngineConnections();
+ } catch (BackendException e) {
+ e.printStackTrace();
+ }
+
+ BackendHelper.updateEngineInstances(engines);
+
+ JsonArray jsonArray = new JsonArray();
+ for (Engine engine : engines) {
+ jsonArray.add(engine.serialize());
+ }
+
+ Ecdar.preferences.put("engines", jsonArray.toString());
+
+ 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;
+ }
+ }
+
+ /**
+ * Resets the engines to the default engines present in the 'default_engines.json' file.
+ */
+ public void resetEnginesToDefault() {
+ updateEnginesInGUI(getPackagedEngines());
+ }
+
+ private void initializeEngineInstanceList() {
+ ArrayList engines;
+
+ // Load engines from preferences or get default
+ var savedEngines = Ecdar.preferences.get("engines", null);
+ if (savedEngines != null) {
+ engines = getEnginesFromJsonArray(
+ JsonParser.parseString(savedEngines).getAsJsonArray());
+ } else {
+ engines = getPackagedEngines();
+ }
+
+ // Style add engine button and handle click event
+ HBox.setHgrow(addEngineButton, Priority.ALWAYS);
+ addEngineButton.setMaxWidth(Double.MAX_VALUE);
+ addEngineButton.setOnMouseClicked((event) -> {
+ EnginePresentation newEnginePresentation = new EnginePresentation();
+ addEnginePresentationToList(newEnginePresentation);
+ });
+
+ updateEnginesInGUI(engines);
+ }
+
+ /**
+ * Clear the engine list and add the newly defined engines to it.
+ *
+ * @param engines The new list of engines
+ */
+ private void updateEnginesInGUI(ArrayList engines) {
+ engineInstanceList.getChildren().clear();
+
+ engines.forEach((engine) -> {
+ EnginePresentation newEnginePresentation = new EnginePresentation(engine);
+
+ // Bind input fields that should not be changed for packaged engines to the locked property of the engine instance
+ newEnginePresentation.getController().engineName.disableProperty().bind(engine.getLockedProperty());
+ newEnginePresentation.getController().pathToEngine.disableProperty().bind(engine.getLockedProperty());
+ newEnginePresentation.getController().pickPathToEngine.disableProperty().bind(engine.getLockedProperty());
+ newEnginePresentation.getController().isLocal.disableProperty().bind(engine.getLockedProperty());
+ addEnginePresentationToList(newEnginePresentation);
+ });
+
+ BackendHelper.updateEngineInstances(engines);
+ }
+
+ /**
+ * Instantiate enginesArray defined in the given JsonArray.
+ *
+ * @param enginesArray The JsonArray containing the enginesArray
+ * @return An ArrayList of the instantiated enginesArray
+ */
+ private ArrayList getEnginesFromJsonArray(JsonArray enginesArray) {
+ ArrayList engines = new ArrayList<>();
+ engineInstanceList.getChildren().clear();
+ enginesArray.forEach((engine) -> {
+ Engine newEngine = new Engine(engine.getAsJsonObject());
+ engines.add(newEngine);
+ });
+
+ return engines;
+ }
+
+ /**
+ * Checks a set of paths to the packaged engines, j-Ecdar and Reveaal, and instantiates them
+ * if one of the related files exists.
+ *
+ * @return The packaged engines
+ */
+ private ArrayList getPackagedEngines() {
+ ArrayList defaultEngines = new ArrayList<>();
+
+ // Add Reveaal engine
+ var reveaal = new Engine();
+ reveaal.setName("Reveaal");
+ reveaal.setLocal(true);
+ reveaal.setDefault(true);
+ reveaal.setPortStart(5040);
+ reveaal.setPortEnd(5042);
+ reveaal.lockInstance();
+ reveaal.setIsThreadSafe(true);
+
+ // Load correct Reveaal executable based on OS
+ List potentialFilesForReveaal = new ArrayList<>();
+ if (SystemUtils.IS_OS_WINDOWS) {
+ potentialFilesForReveaal.add("Reveaal.exe");
+ } else {
+ potentialFilesForReveaal.add("Reveaal");
+ }
+ if (setEnginePathIfFileExists(reveaal, potentialFilesForReveaal)) defaultEngines.add(reveaal);
+
+ // Add jECDAR engine
+ var jEcdar = new Engine();
+ jEcdar.setName("j-Ecdar");
+ jEcdar.setLocal(true);
+ jEcdar.setDefault(false);
+ jEcdar.setPortStart(5042);
+ jEcdar.setPortEnd(5050);
+ jEcdar.lockInstance();
+ jEcdar.setIsThreadSafe(false);
+
+ // Load correct j-Ecdar executable based on OS
+ List potentialFiledForJEcdar = new ArrayList<>();
+ if (SystemUtils.IS_OS_WINDOWS) {
+ potentialFiledForJEcdar.add("j-Ecdar.bat");
+ } else {
+ potentialFiledForJEcdar.add("j-Ecdar");
+ }
+
+ if (setEnginePathIfFileExists(jEcdar, potentialFiledForJEcdar)) defaultEngines.add(jEcdar);
+
+ return defaultEngines;
+ }
+
+ /**
+ * Sets the path to the engine if one of the potential files exists
+ *
+ * @param engine The engine to set the path for
+ * @param potentialFiles List of potential files to use for the engine
+ * @return True if one of the potentialFiles where found in path, false otherwise.
+ * This value also signals whether the engine engineLocation is set
+ */
+ private boolean setEnginePathIfFileExists(Engine engine, List potentialFiles) {
+ engine.setEngineLocation("");
+
+ try {
+ // Get directory containing the bin and lib folders for the executing program
+ String pathToEcdarDirectory = new File(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParentFile().getPath();
+
+ List files = List.of(Objects.requireNonNull(new File(pathToEcdarDirectory).listFiles()));
+ for (File f : files) {
+ if (potentialFiles.contains(f.getName())) {
+ engine.setEngineLocation(f.getAbsolutePath());
+ return true;
+ }
+ }
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ Ecdar.showToast("Unable to get URI of parent directory: \"" + getClass().getProtectionDomain().getCodeSource().getLocation() + "\" due to: " + e.getMessage());
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ Ecdar.showToast("Encountered null reference when trying to get path of executing program");
+ }
+
+ return !engine.getEngineLocation().equals("");
+ }
+
+ /**
+ * Add the new engine instance presentation to the engine options dialog
+ * @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));
+
+ // Set remove engine action to only fire if the engine is not locked
+ newEnginePresentation.getController().removeEngineRippler.setOnMouseClicked((mouseEvent) -> {
+ if (!newEnginePresentation.getController().defaultEngineRadioButton.isSelected()) {
+ engineInstanceList.getChildren().remove(newEnginePresentation);
+ }
+ });
+ newEnginePresentation.getController().defaultEngineRadioButton.setToggleGroup(defaultEngineToggleGroup);
+ }
+
+ /**
+ * Calculated the new position of the engine, 'i' places further down, in the engine list.
+ * The engine presentation is removed and added to the new position.
+ * Given a negative value, the instance is moved up. This function uses loop-around, meaning that:
+ * - If the instance is moved down while already at the bottom of the list, it is placed at the top.
+ * - If the instance is moved up while already at the top of the list, it is placed at the bottom.
+ *
+ * @param enginePresentation The engine presentation to move
+ * @param i The number of steps to move the engine down
+ */
+ private void moveEngineInstance(EnginePresentation enginePresentation, int i) {
+ int currentIndex = engineInstanceList.getChildren().indexOf(enginePresentation);
+ int newIndex = (currentIndex + i) % engineInstanceList.getChildren().size();
+ if (newIndex < 0) {
+ newIndex = engineInstanceList.getChildren().size() - 1;
+ }
+
+ engineInstanceList.getChildren().remove(enginePresentation);
+ engineInstanceList.getChildren().add(newIndex, enginePresentation);
+ }
+
+ /**
+ * Marks input fields in the engineList that contains errors and returns whether any errors were found
+ *
+ * @return whether any errors were found
+ */
+ private boolean engineInstanceListIsErrorFree() {
+ boolean error = true;
+
+ for (Node child : engineInstanceList.getChildren()) {
+ if (child instanceof EnginePresentation) {
+ EngineInstanceController engineInstanceController = ((EnginePresentation) child).getController();
+ error = engineNameIsErrorFree(engineInstanceController) && error;
+ error = portRangeIsErrorFree(engineInstanceController) && error;
+ error = engineInstanceLocationIsErrorFree(engineInstanceController) && error;
+ }
+ }
+
+ return error;
+ }
+
+ private boolean engineNameIsErrorFree(EngineInstanceController engineInstanceController) {
+ String engineName = engineInstanceController.engineName.getText();
+
+ if (engineName.isBlank()) {
+ engineInstanceController.engineNameIssue.setText(ValidationErrorMessages.ENGINE_NAME_EMPTY.toString());
+ engineInstanceController.engineNameIssue.setVisible(true);
+ return false;
+ }
+
+ engineInstanceController.engineNameIssue.setVisible(false);
+ return true;
+ }
+
+ private boolean portRangeIsErrorFree(EngineInstanceController engineInstanceController) {
+ boolean errorFree = true;
+ int portRangeStart = 0, portRangeEnd = 0;
+ engineInstanceController.portRangeStartIssue.setText("");
+ engineInstanceController.portRangeStartIssue.setVisible(false);
+ engineInstanceController.portRangeEndIssue.setText("");
+ engineInstanceController.portRangeEndIssue.setVisible(false);
+ engineInstanceController.portRangeIssue.setVisible(false);
+
+ try {
+ portRangeStart = Integer.parseInt(engineInstanceController.portRangeStart.getText());
+ } catch (NumberFormatException numberFormatException) {
+ engineInstanceController.portRangeStartIssue.setText(ValidationErrorMessages.VALUE_NOT_INTEGER.toString());
+ errorFree = false;
+ }
+
+ try {
+ portRangeEnd = Integer.parseInt(engineInstanceController.portRangeEnd.getText());
+ } catch (NumberFormatException numberFormatException) {
+ engineInstanceController.portRangeEndIssue.setText(ValidationErrorMessages.VALUE_NOT_INTEGER.toString());
+ errorFree = false;
+ }
+
+ Range portRange = Range.between(0, 65535);
+
+ if (!portRange.contains(portRangeStart)) {
+ if (engineInstanceController.portRangeStartIssue.getText().isBlank()) {
+ engineInstanceController.portRangeStartIssue.setText(ValidationErrorMessages.PORT_VALUE_NOT_WITHIN_ACCEPTABLE_RANGE.toString());
+ } else {
+ engineInstanceController.portRangeStartIssue.setText(ValidationErrorMessages.PORT_VALUE_NOT_WITHIN_ACCEPTABLE_RANGE_CONCATINATION.toString());
+ }
+ errorFree = false;
+ }
+ if (!portRange.contains(portRangeEnd)) {
+ if (engineInstanceController.portRangeEndIssue.getText().isBlank()) {
+ engineInstanceController.portRangeEndIssue.setText(ValidationErrorMessages.PORT_VALUE_NOT_WITHIN_ACCEPTABLE_RANGE.toString());
+ } else {
+ engineInstanceController.portRangeEndIssue.setText(ValidationErrorMessages.PORT_VALUE_NOT_WITHIN_ACCEPTABLE_RANGE_CONCATINATION.toString());
+ }
+ errorFree = false;
+ }
+
+ if (portRangeEnd - portRangeStart < 0) {
+ engineInstanceController.portRangeIssue.setText(ValidationErrorMessages.PORT_RANGE_MUST_BE_INCREMENTAL.toString());
+ errorFree = false;
+ }
+
+ engineInstanceController.portRangeStartIssue.setVisible(!errorFree);
+ engineInstanceController.portRangeEndIssue.setVisible(!errorFree);
+ engineInstanceController.portRangeIssue.setVisible(!errorFree);
+
+ return errorFree;
+ }
+
+ private boolean engineInstanceLocationIsErrorFree(EngineInstanceController engineInstanceController) {
+ boolean errorFree = true;
+
+ if (engineInstanceController.isLocal.isSelected()) {
+ if (engineInstanceController.pathToEngine.getText().isBlank()) {
+ engineInstanceController.locationIssue.setText(ValidationErrorMessages.FILE_LOCATION_IS_BLANK.toString());
+ errorFree = false;
+ } else {
+ Path localEnginePath = Paths.get(engineInstanceController.pathToEngine.getText());
+
+ if (!Files.isExecutable(localEnginePath)) {
+ engineInstanceController.locationIssue.setText(ValidationErrorMessages.FILE_DOES_NOT_EXIST_OR_NOT_EXECUTABLE.toString());
+ errorFree = false;
+ }
+ }
+ } else {
+ if (engineInstanceController.address.getText().isBlank()) {
+ engineInstanceController.locationIssue.setText(ValidationErrorMessages.HOST_ADDRESS_IS_BLANK.toString());
+ errorFree = false;
+ } else {
+ try {
+ InetAddress address = InetAddress.getByName(engineInstanceController.address.getText());
+ boolean reachable = address.isReachable(200);
+
+ if (!reachable) {
+ engineInstanceController.locationIssue.setText(ValidationErrorMessages.HOST_NOT_REACHABLE.toString());
+ errorFree = false;
+ }
+
+ } catch (UnknownHostException unknownHostException) {
+ engineInstanceController.locationIssue.setText(ValidationErrorMessages.UNACCEPTABLE_HOST_NAME.toString());
+ errorFree = false;
+ } catch (IOException ioException) {
+ engineInstanceController.locationIssue.setText(ValidationErrorMessages.IO_EXCEPTION_WITH_HOST.toString());
+ errorFree = false;
+ }
+ }
+ }
+
+ engineInstanceController.locationIssue.setVisible(!errorFree);
+
+ return errorFree;
+ }
+
+ private enum ValidationErrorMessages {
+ ENGINE_NAME_EMPTY {
+ @Override
+ public String toString() {
+ return "The engine name cannot be empty";
+ }
+ },
+ VALUE_NOT_INTEGER {
+ @Override
+ public String toString() {
+ return "Value must be integer";
+ }
+ },
+ PORT_RANGE_MUST_BE_INCREMENTAL {
+ @Override
+ public String toString() {
+ return "Start of port range must be greater than end";
+ }
+ },
+ PORT_VALUE_NOT_WITHIN_ACCEPTABLE_RANGE {
+ @Override
+ public String toString() {
+ return "Value must be within range 0 - 65535";
+ }
+ },
+ PORT_VALUE_NOT_WITHIN_ACCEPTABLE_RANGE_CONCATINATION {
+ @Override
+ public String toString() {
+ return " and within range 0 - 65535";
+ }
+ },
+ FILE_LOCATION_IS_BLANK {
+ @Override
+ public String toString() {
+ return "Please specify a file for this engine";
+ }
+ },
+ FILE_DOES_NOT_EXIST_OR_NOT_EXECUTABLE {
+ @Override
+ public String toString() {
+ return "The above file does not exists or ECDAR does not have the privileges to execute it";
+ }
+ },
+ HOST_ADDRESS_IS_BLANK {
+ @Override
+ public String toString() {
+ return "Please specify an address for the external host";
+ }
+ },
+ HOST_NOT_REACHABLE {
+ @Override
+ public String toString() {
+ return "The above address is not reachable. Make sure that the host is correct";
+ }
+ },
+ UNACCEPTABLE_HOST_NAME {
+ @Override
+ public String toString() {
+ return "The above address is not an acceptable host name";
+ }
+ },
+ IO_EXCEPTION_WITH_HOST {
+ @Override
+ public String toString() {
+ return "An I/O exception was encountered while trying to reach the host";
+ }
+ }
+ }
+}
diff --git a/src/main/java/ecdar/controllers/FileController.java b/src/main/java/ecdar/controllers/FileController.java
index 3c76d1b2..6d51f07e 100644
--- a/src/main/java/ecdar/controllers/FileController.java
+++ b/src/main/java/ecdar/controllers/FileController.java
@@ -1,9 +1,27 @@
package ecdar.controllers;
import com.jfoenix.controls.JFXRippler;
+import ecdar.Ecdar;
+import ecdar.abstractions.Component;
+import ecdar.abstractions.EcdarSystem;
+import ecdar.abstractions.HighLevelModel;
+import ecdar.mutation.models.MutationTestPlan;
+import ecdar.utility.colors.Color;
+import ecdar.utility.colors.EnabledColor;
+import ecdar.utility.helpers.ImageScaler;
+import javafx.application.Platform;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.Initializable;
+import javafx.geometry.Insets;
+import javafx.scene.Cursor;
+import javafx.scene.control.Label;
+import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
-import javafx.scene.layout.StackPane;
+import javafx.scene.layout.*;
+import javafx.scene.shape.Circle;
+import org.kordamp.ikonli.javafx.FontIcon;
import java.net.URL;
import java.util.ResourceBundle;
@@ -12,12 +30,132 @@
* Controller for a file in the project pane.
*/
public class FileController implements Initializable {
+ public AnchorPane root;
+ public JFXRippler rippler;
+ public Circle iconBackground;
public JFXRippler moreInformation;
+ public FontIcon moreInformationIcon;
+ public Label fileName;
public ImageView fileImage;
public StackPane fileImageStackPane;
+ private final SimpleObjectProperty model = new SimpleObjectProperty<>(null);
+ private final BooleanProperty isActive = new SimpleBooleanProperty(false);
+ private final EnabledColor color = EnabledColor.getDefault().getLowestIntensity();
+
@Override
public void initialize(URL location, ResourceBundle resources) {
+ Platform.runLater(() -> {
+ initializeIcon();
+ initializeFileIcons();
+ initializeFileName();
+ initializeColors();
+ initializeRippler();
+ initializeMoreInformationButton();
+ });
+ }
+
+ private void initializeMoreInformationButton() {
+ if (getModel() instanceof Component || getModel() instanceof EcdarSystem || getModel() instanceof MutationTestPlan) {
+ moreInformation.setVisible(true);
+ moreInformation.setMaskType(JFXRippler.RipplerMask.CIRCLE);
+ moreInformation.setPosition(JFXRippler.RipplerPos.BACK);
+ moreInformation.setRipplerFill(Color.GREY_BLUE.getColor(Color.Intensity.I500));
+ }
+ }
+
+ private void initializeRippler() {
+ final Color color = Color.GREY_BLUE;
+ final Color.Intensity colorIntensity = Color.Intensity.I400;
+
+ rippler.setMaskType(JFXRippler.RipplerMask.RECT);
+ rippler.setRipplerFill(color.getColor(colorIntensity));
+ rippler.setPosition(JFXRippler.RipplerPos.BACK);
+ }
+
+ private void initializeFileName() {
+ model.get().nameProperty().addListener((obs, oldName, newName) -> fileName.setText(newName));
+ fileName.setText(model.get().getName());
+ }
+
+ private void initializeIcon() {
+ model.get().colorProperty().addListener((obs, oldColor, newColor) -> iconBackground.setFill(newColor.getPaintColor()));
+ iconBackground.setFill(model.get().getColor().getPaintColor());
+ }
+
+ private void initializeColors() {
+ // Update the background when hovered
+ root.setOnMouseEntered(event -> {
+ if (isActive.get()) {
+ setBackground(color.nextIntensity(2));
+ } else {
+ setBackground(color.nextIntensity());
+ }
+ root.setCursor(Cursor.HAND);
+ });
+
+ root.setOnMouseExited(event -> {
+ if (isActive.get()) {
+ setBackground(color.nextIntensity());
+ } else {
+ setBackground(color);
+ }
+ root.setCursor(Cursor.DEFAULT);
+ });
+
+ // Update the background initially
+ setBackground(color);
+ }
+
+ /**
+ * Initialises the icons for the three different file types in Ecdar: Component, System and Declarations
+ */
+ private void initializeFileIcons() {
+ if (model.get() instanceof Component) {
+ fileImage.setImage(new Image(Ecdar.class.getResource("component_frame.png").toExternalForm()));
+ } else if (model.get() instanceof EcdarSystem) {
+ fileImage.setImage(new Image(Ecdar.class.getResource("system_frame.png").toExternalForm()));
+ } else if (model.get() instanceof MutationTestPlan) {
+ fileImage.setImage(new Image(Ecdar.class.getResource("test_frame.png").toExternalForm()));
+ } else {
+ fileImage.setImage(new Image(Ecdar.class.getResource("description_frame.png").toExternalForm()));
+ }
+
+ ImageScaler.fitImageToPane(fileImage, fileImageStackPane);
+ }
+
+ private void setBackground(EnabledColor newColor) {
+ root.setBackground(new Background(new BackgroundFill(
+ newColor.getPaintColor(),
+ CornerRadii.EMPTY,
+ Insets.EMPTY
+ )));
+
+ root.setBorder(new Border(new BorderStroke(
+ newColor.getStrokeColor(),
+ BorderStrokeStyle.SOLID,
+ CornerRadii.EMPTY,
+ new BorderWidths(0, 0, 1, 0)
+ )));
+
+ moreInformationIcon.setFill(newColor.nextIntensity(5).getPaintColor());
+ }
+
+ public void setModel(HighLevelModel newModel) {
+ model.set(newModel);
+ }
+
+ public HighLevelModel getModel() {
+ return model.get();
+ }
+
+ public void setIsActive(boolean active) {
+ isActive.set(active);
+ if (active) {
+ setBackground(color.nextIntensity());
+ } else {
+ setBackground(color);
+ }
}
}
diff --git a/src/main/java/ecdar/controllers/HighLevelModelController.java b/src/main/java/ecdar/controllers/HighLevelModelController.java
new file mode 100644
index 00000000..c346a268
--- /dev/null
+++ b/src/main/java/ecdar/controllers/HighLevelModelController.java
@@ -0,0 +1,7 @@
+package ecdar.controllers;
+
+import ecdar.abstractions.HighLevelModel;
+
+abstract public class HighLevelModelController {
+ public abstract HighLevelModel getModel();
+}
diff --git a/src/main/java/ecdar/controllers/LocationController.java b/src/main/java/ecdar/controllers/LocationController.java
index 34b62838..d6392b4f 100644
--- a/src/main/java/ecdar/controllers/LocationController.java
+++ b/src/main/java/ecdar/controllers/LocationController.java
@@ -8,6 +8,7 @@
import ecdar.presentations.*;
import ecdar.utility.UndoRedoStack;
import ecdar.utility.colors.Color;
+import ecdar.utility.colors.EnabledColor;
import ecdar.utility.helpers.ItemDragHelper;
import ecdar.utility.helpers.SelectHelper;
import ecdar.utility.keyboard.Keybind;
@@ -36,8 +37,9 @@
import java.util.*;
import java.util.function.Consumer;
-public class LocationController implements Initializable, SelectHelper.ItemSelectable, Nudgeable {
+import static ecdar.presentations.ModelPresentation.TOOLBAR_HEIGHT;
+public class LocationController implements Initializable, SelectHelper.ItemSelectable, Nudgeable {
private static final Map invalidNameError = new HashMap<>();
private final ObjectProperty location = new SimpleObjectProperty<>();
@@ -64,7 +66,6 @@ public class LocationController implements Initializable, SelectHelper.ItemSelec
public Line prohibitedLocStrikeThrough;
private DropDownMenu dropDownMenu;
- private boolean dropDownMenuInitialized = false;
@Override
public void initialize(final URL location, final ResourceBundle resources) {
@@ -84,18 +85,15 @@ public void initialize(final URL location, final ResourceBundle resources) {
}
private void initializeSelectListener() {
- SelectHelper.elementsToBeSelected.addListener(new ListChangeListener() {
- @Override
- public void onChanged(final Change extends Nearable> c) {
- while (c.next()) {
- if (c.getAddedSize() == 0) return;
-
- for (final Nearable nearable : SelectHelper.elementsToBeSelected) {
- if (nearable instanceof Location) {
- if (nearable.equals(getLocation())) {
- SelectHelper.addToSelection(LocationController.this);
- break;
- }
+ SelectHelper.elementsToBeSelected.addListener((ListChangeListener) c -> {
+ while (c.next()) {
+ if (c.getAddedSize() == 0) return;
+
+ for (final Nearable nearable : SelectHelper.elementsToBeSelected) {
+ if (nearable instanceof Location) {
+ if (nearable.equals(getLocation())) {
+ SelectHelper.addToSelection(LocationController.this);
+ break;
}
}
}
@@ -108,15 +106,15 @@ public void initializeDropDownMenu() {
dropDownMenu.addClickableAndDisableableListElement("Draw Edge", getLocation().getIsLocked(),
(event) -> {
- final Edge newEdge = new Edge(getLocation(), EcdarController.getGlobalEdgeStatus());
+ final Edge newEdge = new Edge(getLocation(), EcdarController.getGlobalEdgeStatus());
- KeyboardTracker.registerKeybind(KeyboardTracker.ABANDON_EDGE, new Keybind(new KeyCodeCombination(KeyCode.ESCAPE), () -> {
- getComponent().removeEdge(newEdge);
- }));
- getComponent().addEdge(newEdge);
- dropDownMenu.hide();
- }
- );
+ KeyboardTracker.registerKeybind(KeyboardTracker.ABANDON_EDGE, new Keybind(new KeyCodeCombination(KeyCode.ESCAPE), () -> {
+ getComponent().removeEdge(newEdge);
+ }));
+ getComponent().addEdge(newEdge);
+ dropDownMenu.hide();
+ }
+ );
dropDownMenu.addClickableAndDisableableListElement("Add Nickname",
getLocation().nicknameProperty().isNotEmpty().or(nicknameTag.textFieldFocusProperty()),
@@ -191,8 +189,25 @@ public void initializeDropDownMenu() {
dropDownMenu.addSpacerElement();
- dropDownMenu.addColorPicker(getLocation(), (color, intensity) -> {
- getLocation().setColorIntensity(intensity);
+ dropDownMenu.addClickableListElement("Is " + getLocation().getId() + " reachable?", event -> {
+ dropDownMenu.hide();
+ // Generate the query from the engine
+ final String reachabilityQuery = getComponent().getName() + "." + getLocation().getId();
+
+ // Add proper comment
+ final String reachabilityComment = "Is " + getLocation().getMostDescriptiveIdentifier() + " reachable?";
+
+ // Add new query for this location
+ final Query query = new Query(reachabilityQuery, reachabilityComment, QueryState.UNKNOWN);
+ query.setType(QueryType.REACHABILITY);
+ Ecdar.getProject().getQueries().add(query);
+ query.execute();
+ dropDownMenu.hide();
+ });
+
+ dropDownMenu.addSpacerElement();
+
+ dropDownMenu.addColorPicker(getLocation(), (color) -> {
getLocation().setColor(color);
});
@@ -262,17 +277,11 @@ public Location getLocation() {
public void setLocation(final Location location) {
this.location.set(location);
-
- if (ComponentController.isPlacingLocation()) {
- root.layoutXProperty().bindBidirectional(location.xProperty());
- root.layoutYProperty().bindBidirectional(location.yProperty());
- } else {
- root.setLayoutX(location.getX());
- root.setLayoutY(location.getY());
- location.xProperty().bindBidirectional(root.layoutXProperty());
- location.yProperty().bindBidirectional(root.layoutYProperty());
- root.setPlaced(true);
- }
+ root.setLayoutX(location.getX());
+ root.setLayoutY(location.getY());
+ location.xProperty().bindBidirectional(root.layoutXProperty());
+ location.yProperty().bindBidirectional(root.layoutYProperty());
+ root.setPlaced(true);
}
public ObjectProperty locationProperty() {
@@ -303,7 +312,7 @@ private void locationExited() {
@FXML
private void mouseEntered() {
- if(!this.root.isInteractable()) return;
+ if (!this.root.isInteractable()) return;
circle.setCursor(Cursor.HAND);
this.root.animateHoverEntered();
@@ -330,7 +339,7 @@ private void mouseEntered() {
@FXML
private void mouseExited() {
final LocationPresentation locationPresentation = this.root;
- if(!locationPresentation.isInteractable()) return;
+ if (!locationPresentation.isInteractable()) return;
circle.setCursor(Cursor.DEFAULT);
locationPresentation.animateHoverExited();
@@ -409,7 +418,7 @@ private void initializeMouseControls() {
}
// Otherwise, select the location
else {
- if(root.isInteractable()) {
+ if (root.isInteractable()) {
if (event.isShortcutDown()) {
if (SelectHelper.getSelectedElements().contains(this)) {
SelectHelper.deselect(this);
@@ -429,7 +438,7 @@ private void initializeMouseControls() {
final double minY = Ecdar.CANVAS_PADDING * 4;
final double maxY = getComponent().getBox().getHeight() - Ecdar.CANVAS_PADDING * 2;
- if(root.getLayoutX() >= minX && root.getLayoutX() <= maxX && root.getLayoutY() >= minY && root.getLayoutY() <= maxY) {
+ if (root.getLayoutX() >= minX && root.getLayoutX() <= maxX && root.getLayoutY() >= minY && root.getLayoutY() <= maxY) {
// Unbind presentation root x and y coordinates (bind the view properly to enable dragging)
root.layoutXProperty().unbind();
root.layoutYProperty().unbind();
@@ -440,7 +449,6 @@ private void initializeMouseControls() {
// Notify that the location was placed
root.setPlaced(true);
- ComponentController.setPlacingLocation(null);
KeyboardTracker.unregisterKeybind(KeyboardTracker.ABANDON_LOCATION);
} else {
root.shake();
@@ -449,37 +457,31 @@ private void initializeMouseControls() {
};
locationProperty().addListener((obs, oldLocation, newLocation) -> {
- if(newLocation == null) return;
+ if (newLocation == null) return;
root.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClicked::accept);
ItemDragHelper.makeDraggable(root, this::getDragBounds);
});
}
@Override
- public void color(final Color color, final Color.Intensity intensity) {
+ public void color(final EnabledColor color) {
final Location location = getLocation();
// Set the color of the location
- location.setColorIntensity(intensity);
location.setColor(color);
}
@Override
- public Color getColor() {
+ public EnabledColor getColor() {
return getLocation().getColor();
}
- @Override
- public Color.Intensity getColorIntensity() {
- return getLocation().getColorIntensity();
- }
-
@Override
public ItemDragHelper.DragBounds getDragBounds() {
final int PADDING = 5;
final ObservableDoubleValue minX = new SimpleDoubleProperty(location.get().getRadius() + PADDING);
final ObservableDoubleValue maxX = getComponent().getBox().getWidthProperty().subtract(location.get().getRadius() + PADDING);
- final ObservableDoubleValue minY = new SimpleDoubleProperty(location.get().getRadius() + PADDING);
+ final ObservableDoubleValue minY = new SimpleDoubleProperty(location.get().getRadius() + TOOLBAR_HEIGHT + PADDING);
final ObservableDoubleValue maxY = getComponent().getBox().getHeightProperty().subtract(location.get().getRadius() + PADDING);
return new ItemDragHelper.DragBounds(minX, maxX, minY, maxY);
}
@@ -507,14 +509,15 @@ public boolean nudge(final NudgeDirection direction) {
return oldX != newX || oldY != newY;
}
- /**'
+ /**
* Method which has the logical expression for the shortcut of adding a new edge,
* checks whether the location is locked and the correct buttons are held down
+ *
* @param event the mouse event
* @return the result of the boolean expression, false if the location is locked or the incorrect buttons are held down,
* true if the correct buttons are down
*/
- private boolean canCreateEdgeShortcut(MouseEvent event){
+ private boolean canCreateEdgeShortcut(MouseEvent event) {
return (!getLocation().getIsLocked().get() &&
((event.isShiftDown() && event.getButton().equals(MouseButton.PRIMARY)) || event.getButton().equals(MouseButton.MIDDLE)));
}
diff --git a/src/main/java/ecdar/controllers/MessageCollectionController.java b/src/main/java/ecdar/controllers/MessageCollectionController.java
new file mode 100644
index 00000000..c8269bc2
--- /dev/null
+++ b/src/main/java/ecdar/controllers/MessageCollectionController.java
@@ -0,0 +1,123 @@
+package ecdar.controllers;
+
+import ecdar.Ecdar;
+import ecdar.abstractions.Component;
+import ecdar.code_analysis.CodeAnalysis;
+import ecdar.presentations.MessagePresentation;
+import ecdar.utility.colors.Color;
+import ecdar.utility.colors.EnabledColor;
+import javafx.beans.InvalidationListener;
+import javafx.beans.property.SimpleListProperty;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.event.EventHandler;
+import javafx.fxml.Initializable;
+import javafx.scene.Cursor;
+import javafx.scene.control.Label;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.VBox;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Line;
+
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.function.Consumer;
+
+public class MessageCollectionController implements Initializable {
+ public VBox root;
+ public Circle indicator;
+ public Label headline;
+ public Line line;
+ public VBox messageBox;
+
+ private Component component;
+ private final ObservableList messages = new SimpleListProperty<>();
+ private Map messageMessagePresentationMap;
+
+ @Override
+ public void initialize(URL location, ResourceBundle resources) {
+ initializeHeadline(component);
+ initializeLine();
+ initializeErrorsListener();
+ }
+
+ private void initializeErrorsListener() {
+ messageMessagePresentationMap = new HashMap<>();
+
+ final Consumer addMessage = (message) -> {
+ final MessagePresentation messagePresentation = new MessagePresentation(message);
+ messageMessagePresentationMap.put(message, messagePresentation);
+ messageBox.getChildren().add(messagePresentation);
+ };
+
+ messages.forEach(addMessage);
+ messages.addListener((ListChangeListener) c -> {
+ while (c.next()) {
+ c.getAddedSubList().forEach(addMessage);
+
+ c.getRemoved().forEach(message -> {
+ messageBox.getChildren().remove(messageMessagePresentationMap.get(message));
+ messageMessagePresentationMap.remove(message);
+ });
+ }
+ });
+ }
+
+ private void initializeLine() {
+ messageBox.getChildren().addListener((InvalidationListener) observable -> line.setEndY(messageBox.getChildren().size() * 23 + 8));
+ }
+
+ private void initializeHeadline(final Component component) {
+ line.setStroke(Color.GREY.getColor(Color.Intensity.I400));
+
+ // This is an project wide message that is not specific to a component
+ if (component == null) {
+ headline.setText("Project");
+ return;
+ }
+
+ headline.setText(component.getName());
+ headline.textProperty().bind(component.nameProperty());
+
+ final EventHandler onMouseEntered = event -> {
+ root.setCursor(Cursor.HAND);
+ headline.setStyle("-fx-underline: true;");
+ };
+
+ final EventHandler onMouseExited = event -> {
+ root.setCursor(Cursor.DEFAULT);
+ headline.setStyle("-fx-underline: false;");
+ };
+
+ 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));
+
+ headline.setOnMouseEntered(onMouseEntered);
+ headline.setOnMouseExited(onMouseExited);
+ headline.setOnMousePressed(onMousePressed);
+ indicator.setOnMouseEntered(onMouseEntered);
+ indicator.setOnMouseExited(onMouseExited);
+ indicator.setOnMousePressed(onMousePressed);
+
+ final Consumer updateColor = (color) -> {
+ indicator.setFill(color.getPaintColor());
+ };
+
+ updateColor.accept(component.getColor());
+ component.colorProperty().addListener((observable, oldColor, newColor) -> updateColor.accept(newColor));
+ }
+
+ public void setComponent(Component newComponent) {
+ component = newComponent;
+ }
+
+ public void setMessages(List newMessages) {
+ if (!newMessages.isEmpty()) messages.setAll(newMessages);
+ }
+}
diff --git a/src/main/java/ecdar/controllers/ModeController.java b/src/main/java/ecdar/controllers/ModeController.java
new file mode 100644
index 00000000..d1238118
--- /dev/null
+++ b/src/main/java/ecdar/controllers/ModeController.java
@@ -0,0 +1,8 @@
+package ecdar.controllers;
+
+import javafx.scene.layout.StackPane;
+
+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 f0b91496..6e378f4a 100644
--- a/src/main/java/ecdar/controllers/ModelController.java
+++ b/src/main/java/ecdar/controllers/ModelController.java
@@ -1,16 +1,31 @@
package ecdar.controllers;
-import ecdar.abstractions.HighLevelModelObject;
+import ecdar.Ecdar;
+import ecdar.abstractions.Box;
+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;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.geometry.Insets;
+import javafx.scene.Cursor;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
+import static ecdar.presentations.ModelPresentation.CORNER_SIZE;
+
/**
* Controller for a high level model, such as a component or a system.
*/
-public abstract class ModelController {
+public abstract class ModelController extends HighLevelModelController {
public StackPane root;
public Rectangle background;
public BorderPane frame;
@@ -21,8 +36,6 @@ public abstract class ModelController {
public BorderPane toolbar;
public JFXTextField name;
- public abstract HighLevelModelObject getModel();
-
/**
* Hides the border and background.
*/
@@ -40,4 +53,261 @@ void showBorderAndBackground() {
topLeftLine.setVisible(true);
background.setVisible(true);
}
+
+ abstract double getDragAnchorMinWidth();
+ abstract double getDragAnchorMinHeight();
+
+ /**
+ * Initializes this.
+ * @param box the box of the model
+ */
+ void initialize(final Box box) {
+ initializeName();
+ initializeDimensions(box);
+ initializesBottomDragAnchor(box);
+ initializesRightDragAnchor(box);
+ initializesCornerDragAnchor(box);
+ }
+
+ /**
+ * Initializes handling of name.
+ */
+ private void initializeName() {
+ final HighLevelModel model = getModel();
+
+ final BooleanProperty initialized = new SimpleBooleanProperty(false);
+
+ name.focusedProperty().addListener((observable, oldValue, newValue) -> {
+ if (newValue && !initialized.get()) {
+ root.requestFocus();
+ initialized.setValue(true);
+ }
+ });
+
+ // Set the text field to the name in the model, and bind the model to the text field
+ name.setText(model.getName());
+ name.textProperty().addListener((obs, oldName, newName) -> {
+ if (StringValidator.validateComponentName(newName)) {
+ model.nameProperty().unbind();
+ model.setName(newName);
+ } else {
+ name.setText(model.getName());
+ Ecdar.showToast("Component names cannot contain '.'");
+ }
+ });
+
+ final Runnable updateColor = () -> {
+ final EnabledColor color = model.getColor();
+
+ // Set the text color for the label
+ name.setStyle("-fx-text-fill: " + color.color.getTextColorRgbaString(color.intensity) + ";");
+ name.setFocusColor(color.getTextColor());
+ name.setUnFocusColor(javafx.scene.paint.Color.TRANSPARENT);
+ };
+
+ model.colorProperty().addListener(observable -> updateColor.run());
+ updateColor.run();
+
+ 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());
+ });
+ }
+
+ /**
+ * Sets the width and the height of the view to the values in the abstraction.
+ * @param box The dimensions to set
+ */
+ void initializeDimensions(final Box box) {
+ // Ensure that the component snaps to the grid
+ if (box.getX() == 0 && box.getY() == 0) {
+ box.setX(Ecdar.CANVAS_PADDING * 0.5);
+ box.setY(Ecdar.CANVAS_PADDING * 0.5);
+ }
+
+ // Bind the position of the abstraction to the values in the view
+ root.layoutXProperty().set(box.getX());
+ root.layoutYProperty().set(box.getY());
+ box.getXProperty().bindBidirectional(root.layoutXProperty());
+ box.getYProperty().bindBidirectional(root.layoutYProperty());
+
+ root.setMinWidth(box.getWidth());
+ root.setMaxWidth(box.getWidth());
+ root.setMinHeight(box.getHeight());
+ root.setMaxHeight(box.getHeight());
+ root.minHeightProperty().bindBidirectional(box.getHeightProperty());
+ root.maxHeightProperty().bindBidirectional(box.getHeightProperty());
+ root.minWidthProperty().bindBidirectional(box.getWidthProperty());
+ root.maxWidthProperty().bindBidirectional(box.getWidthProperty());
+ }
+
+ /**
+ * Initializes the right drag anchor.
+ * @param box the box of the model
+ */
+ private void initializesRightDragAnchor(final Box box) {
+ final BooleanProperty wasResized = new SimpleBooleanProperty(false);
+ rightAnchor.setCursor(Cursor.E_RESIZE);
+
+ // Bind the place and size of bottom anchor
+ rightAnchor.setWidth(5);
+ rightAnchor.heightProperty().bind(box.getHeightProperty());
+
+ final DoubleProperty prevX = new SimpleDoubleProperty();
+ final DoubleProperty prevWidth = new SimpleDoubleProperty();
+
+ rightAnchor.setOnMousePressed(event -> {
+ event.consume();
+
+ prevX.set(event.getScreenX());
+ prevWidth.set(box.getWidth());
+ });
+
+ rightAnchor.setOnMouseDragged(event -> {
+ double diff = event.getScreenX() - prevX.get();
+ diff -= diff % Ecdar.CANVAS_PADDING;
+
+ final double newWidth = prevWidth.get() + diff;
+ final double minWidth = getDragAnchorMinWidth();
+
+ // Move the model left or right to account for new height (needed because model is centered in parent)
+ root.setTranslateX(root.getTranslateX() + (Math.max(newWidth, minWidth) - box.getWidth()) / 2);
+ box.setWidth(Math.max(newWidth, minWidth));
+ wasResized.set(true);
+ });
+
+ rightAnchor.setOnMouseReleased(event -> {
+ if (!wasResized.get()) return;
+ final double previousWidth = prevWidth.doubleValue();
+ final double currentWidth = box.getWidth();
+
+ // If no difference do not save change
+ if (previousWidth == currentWidth) return;
+
+ UndoRedoStack.pushAndPerform(() -> { // Perform
+ box.setWidth(currentWidth);
+ }, () -> { // Undo
+ box.setWidth(previousWidth);
+ },
+ "Component width resized",
+ "settings-overscan"
+ );
+
+ wasResized.set(false);
+ });
+ }
+
+ /**
+ * Initializes the bottom drag anchor.
+ * @param box the box of the model
+ */
+ private void initializesBottomDragAnchor(final Box box) {
+ final BooleanProperty wasResized = new SimpleBooleanProperty(false);
+ bottomAnchor.setCursor(Cursor.S_RESIZE);
+
+ // Bind the place and size of bottom anchor
+ bottomAnchor.widthProperty().bind(box.getWidthProperty());
+ bottomAnchor.setHeight(5);
+
+ final DoubleProperty prevY = new SimpleDoubleProperty();
+ final DoubleProperty prevHeight = new SimpleDoubleProperty();
+
+ bottomAnchor.setOnMousePressed(event -> {
+ event.consume();
+
+ prevY.set(event.getScreenY());
+ prevHeight.set(box.getHeight());
+ });
+
+ bottomAnchor.setOnMouseDragged(event -> {
+ double diff = event.getScreenY() - prevY.get();
+ final double newHeight = prevHeight.get() + diff;
+ final double minHeight = getDragAnchorMinHeight();
+
+ // Move the model up or down to account for new height (needed because model is centered in parent)
+ root.setTranslateY(root.getTranslateY() + (Math.max(newHeight, minHeight) - box.getHeight()) / 2);
+ box.setHeight(Math.max(newHeight, minHeight));
+ wasResized.set(true);
+ });
+
+ bottomAnchor.setOnMouseReleased(event -> {
+ if (!wasResized.get()) return;
+ final double previousHeight = prevHeight.doubleValue();
+ final double currentHeight = box.getHeight();
+
+ // If no difference do not save change
+ if (previousHeight == currentHeight) return;
+
+ UndoRedoStack.pushAndPerform(() -> { // Perform
+ box.setHeight(currentHeight);
+ }, () -> { // Undo
+ box.setHeight(previousHeight);
+ },
+ "Component height resized",
+ "settings-overscan"
+ );
+
+ wasResized.set(false);
+ });
+ }
+
+ private void initializesCornerDragAnchor(final Box box) {
+ final BooleanProperty wasResized = new SimpleBooleanProperty(false);
+ cornerAnchor.setCursor(Cursor.SE_RESIZE);
+
+ // Bind the place and size of bottom anchor
+ cornerAnchor.setWidth(10);
+ cornerAnchor.setHeight(10);
+
+ final DoubleProperty prevX = new SimpleDoubleProperty();
+ final DoubleProperty prevY = new SimpleDoubleProperty();
+ final DoubleProperty prevWidth = new SimpleDoubleProperty();
+ final DoubleProperty prevHeight = new SimpleDoubleProperty();
+
+ cornerAnchor.setOnMousePressed(event -> {
+ event.consume();
+
+ prevX.set(event.getScreenX());
+ prevWidth.set(box.getWidth());
+ prevY.set(event.getScreenY());
+ prevHeight.set(box.getHeight());
+ });
+
+ cornerAnchor.setOnMouseDragged(event -> {
+ double xDiff = (event.getScreenX() - prevX.get()) / Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get(); // ToDo NIELS: Fix dependency
+ final double newWidth = Math.max(prevWidth.get() + xDiff, getDragAnchorMinWidth());
+ box.setWidth(newWidth);
+
+ double yDiff = (event.getScreenY() - prevY.get()) / Ecdar.getPresentation().getController().getEditorPresentation().getController().getActiveCanvasZoomFactor().get();
+ final double newHeight = Math.max(prevHeight.get() + yDiff, getDragAnchorMinHeight());
+ box.setHeight(newHeight);
+
+ wasResized.set(true);
+ });
+
+ cornerAnchor.setOnMouseReleased(event -> {
+ if (!wasResized.get()) return;
+ final double previousWidth = prevWidth.doubleValue();
+ final double currentWidth = box.getWidth();
+ final double previousHeight = prevHeight.doubleValue();
+ final double currentHeight = box.getHeight();
+
+ // If no difference do not save change
+ if (previousWidth == currentWidth && previousHeight == currentHeight) return;
+
+ UndoRedoStack.pushAndPerform(() -> { // Perform
+ box.setWidth(currentWidth);
+ box.setHeight(currentHeight);
+ }, () -> { // Undo
+ box.setWidth(previousWidth);
+ box.setHeight(previousHeight);
+ },
+ "Component resized",
+ "settings-overscan"
+ );
+
+ wasResized.set(false);
+ });
+ }
}
diff --git a/src/main/java/ecdar/controllers/MultiSyncTagController.java b/src/main/java/ecdar/controllers/MultiSyncTagController.java
index fd2d5bae..6a7bfb8a 100644
--- a/src/main/java/ecdar/controllers/MultiSyncTagController.java
+++ b/src/main/java/ecdar/controllers/MultiSyncTagController.java
@@ -14,7 +14,6 @@
import java.util.ResourceBundle;
public class MultiSyncTagController implements Initializable {
-
public VBox syncList;
public BorderPane topbar;
public BorderPane frame;
diff --git a/src/main/java/ecdar/controllers/NailController.java b/src/main/java/ecdar/controllers/NailController.java
index e3222c61..3f28556a 100644
--- a/src/main/java/ecdar/controllers/NailController.java
+++ b/src/main/java/ecdar/controllers/NailController.java
@@ -6,6 +6,7 @@
import ecdar.presentations.*;
import ecdar.utility.UndoRedoStack;
import ecdar.utility.colors.Color;
+import ecdar.utility.colors.EnabledColor;
import ecdar.utility.helpers.ItemDragHelper;
import ecdar.utility.helpers.SelectHelper;
import ecdar.utility.keyboard.NudgeDirection;
@@ -244,20 +245,15 @@ public ObjectProperty edgeProperty() {
}
@Override
- public void color(final Color color, final Color.Intensity intensity) {
+ public void color(final EnabledColor color) {
// Do nothing. A nail cannot be colored, but can be colored as selected
}
@Override
- public Color getColor() {
+ public EnabledColor getColor() {
return getComponent().getColor();
}
- @Override
- public Color.Intensity getColorIntensity() {
- return getComponent().getColorIntensity();
- }
-
@Override
public ItemDragHelper.DragBounds getDragBounds() {
final int PADDING = 5;
diff --git a/src/main/java/ecdar/controllers/ProcessController.java b/src/main/java/ecdar/controllers/ProcessController.java
index dc94965c..aa79a1b1 100755
--- a/src/main/java/ecdar/controllers/ProcessController.java
+++ b/src/main/java/ecdar/controllers/ProcessController.java
@@ -1,10 +1,13 @@
package ecdar.controllers;
import com.jfoenix.controls.JFXRippler;
+import ecdar.Ecdar;
import ecdar.abstractions.*;
import ecdar.presentations.ComponentPresentation;
+import ecdar.presentations.ModelPresentation;
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;
@@ -18,6 +21,7 @@
import javafx.scene.shape.Circle;
import javafx.util.Duration;
+import org.checkerframework.checker.units.qual.K;
import org.fxmisc.richtext.LineNumberFactory;
import org.fxmisc.richtext.StyleClassedTextArea;
import org.kordamp.ikonli.javafx.FontIcon;
@@ -50,7 +54,7 @@ public class ProcessController extends ModelController implements Initializable
@Override
public void initialize(final URL location, final ResourceBundle resources) {
- component = new SimpleObjectProperty<>(new Component(true));
+ component = new SimpleObjectProperty<>(new Component());
// add line numbers to the declaration text area
declarationTextArea.setParagraphGraphicFactory(LineNumberFactory.get(declarationTextArea));
initializeValues();
@@ -205,7 +209,7 @@ public void setComponent(final Component component){
});
declarationTextArea.appendText(component.getDeclarationsText());
- declarationTextArea.setStyleSpans(0, ComponentPresentation.computeHighlighting(getComponent().getDeclarationsText()));
+ declarationTextArea.setStyleSpans(0, UPPAALSyntaxHighlighter.computeHighlighting(getComponent().getDeclarationsText()));
declarationTextArea.getStyleClass().add("component-declaration");
}
@@ -249,7 +253,7 @@ public Component getComponent(){
}
@Override
- public HighLevelModelObject getModel() {
+ public HighLevelModel getModel() {
return component.get();
}
@@ -260,4 +264,49 @@ public ObservableMap getVariables() {
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 b2e3d4e6..c040b5c9 100644
--- a/src/main/java/ecdar/controllers/ProjectPaneController.java
+++ b/src/main/java/ecdar/controllers/ProjectPaneController.java
@@ -4,23 +4,31 @@
import ecdar.Ecdar;
import ecdar.abstractions.Component;
import ecdar.abstractions.EcdarSystem;
-import ecdar.abstractions.HighLevelModelObject;
+import ecdar.abstractions.HighLevelModel;
+import ecdar.abstractions.Project;
+import ecdar.mutation.MutationTestPlanPresentation;
import ecdar.mutation.models.MutationTestPlan;
-import ecdar.presentations.DropDownMenu;
-import ecdar.presentations.FilePresentation;
+import ecdar.presentations.*;
import ecdar.utility.UndoRedoStack;
import com.jfoenix.controls.JFXPopup;
import com.jfoenix.controls.JFXRippler;
import com.jfoenix.controls.JFXTextArea;
+import ecdar.utility.colors.EnabledColor;
+import ecdar.utility.keyboard.Keybind;
+import ecdar.utility.keyboard.KeyboardTracker;
import javafx.application.Platform;
+import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Insets;
-import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.ImageView;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCodeCombination;
+import javafx.scene.input.KeyCombination;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@@ -28,10 +36,7 @@
import org.kordamp.ikonli.material.Material;
import java.net.URL;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.ResourceBundle;
+import java.util.*;
public class ProjectPaneController implements Initializable {
public StackPane root;
@@ -52,65 +57,71 @@ public class ProjectPaneController implements Initializable {
public ImageView createSystemImage;
public StackPane createSystemPane;
- private final HashMap modelPresentationMap = new HashMap<>();
+
+ public final Project project = new Project();
+ private final HashMap modelPresentationMap = new HashMap<>();
+ private final ObservableList componentPresentations = FXCollections.observableArrayList();
@Override
public void initialize(final URL location, final ResourceBundle resources) {
- // Bind global declarations and add mouse event
- final FilePresentation globalDclPresentation = new FilePresentation(Ecdar.getProject().getGlobalDeclarations());
- globalDclPresentation.setOnMousePressed(event -> {
- event.consume();
- EcdarController.setActiveModelForActiveCanvas(Ecdar.getProject().getGlobalDeclarations());
- updateColorsOnFilePresentations();
+ Platform.runLater(() -> {
+ // Bind global declarations and add mouse event
+ final DeclarationsPresentation globalDeclarationsPresentation = new DeclarationsPresentation(project.getGlobalDeclarations());
+ final FilePresentation globalDclPresentation = new FilePresentation(project.getGlobalDeclarations());
+ modelPresentationMap.put(globalDeclarationsPresentation, globalDclPresentation);
+ globalDclPresentation.setOnMousePressed(event -> {
+ Ecdar.getPresentation().getController().getEditorPresentation().getController().setActiveModelPresentationForActiveCanvas(globalDeclarationsPresentation);
+ });
+
+ filesList.getChildren().add(globalDclPresentation);
});
- filesList.getChildren().add(globalDclPresentation);
- Ecdar.getProject().getComponents().addListener(new ListChangeListener() {
- @Override
- public void onChanged(final Change extends Component> c) {
- while (c.next()) {
- c.getAddedSubList().forEach(o -> handleAddedModel(o));
- c.getRemoved().forEach(o -> handleRemovedModel(o));
+ initializeComponentHandling();
+ initializeSystemHandling();
+ initializeMutationTestPlanHandling();
+ initializeCreateComponentKeybinding();
+ resetProject();
- // Sort the children alphabetically
- sortPresentations();
- }
- }
+ Platform.runLater(() -> {
+ final var initializedModelPresentation = modelPresentationMap.keySet().stream().filter(mp -> mp instanceof ComponentPresentation).findFirst().orElse(null);
+ Ecdar.getPresentation().getController().getEditorPresentation().getController().setActiveModelPresentationForActiveCanvas(initializedModelPresentation);
});
+ }
- Ecdar.getProject().getTempComponents().addListener((ListChangeListener