aboutsummaryrefslogtreecommitdiffstats
path: root/src/jcgp
diff options
context:
space:
mode:
Diffstat (limited to 'src/jcgp')
-rw-r--r--src/jcgp/gui/GUI.java477
-rw-r--r--src/jcgp/gui/console/ConsolePane.java122
-rw-r--r--src/jcgp/gui/constants/Constants.java170
-rw-r--r--src/jcgp/gui/constants/Position.java103
-rw-r--r--src/jcgp/gui/dragresize/HorizontalDragResize.java131
-rw-r--r--src/jcgp/gui/dragresize/VerticalDragResize.java132
-rw-r--r--src/jcgp/gui/handlers/InputHandlers.java56
-rw-r--r--src/jcgp/gui/handlers/NodeHandlers.java164
-rw-r--r--src/jcgp/gui/handlers/OutputHandlers.java84
-rw-r--r--src/jcgp/gui/handlers/Target.java70
-rw-r--r--src/jcgp/gui/population/ChromosomePane.java140
-rw-r--r--src/jcgp/gui/population/FunctionSelector.java80
-rw-r--r--src/jcgp/gui/population/GUIConnection.java32
-rw-r--r--src/jcgp/gui/population/GUIGene.java187
-rw-r--r--src/jcgp/gui/population/GUIInput.java70
-rw-r--r--src/jcgp/gui/population/GUIMutable.java20
-rw-r--r--src/jcgp/gui/population/GUINode.java134
-rw-r--r--src/jcgp/gui/population/GUIOutput.java79
-rw-r--r--src/jcgp/gui/population/PopulationPane.java75
-rw-r--r--src/jcgp/gui/settings/SettingsPane.java595
-rw-r--r--src/jcgp/gui/settings/parameters/GUIBooleanParameter.java82
-rw-r--r--src/jcgp/gui/settings/parameters/GUIDoubleParameter.java110
-rw-r--r--src/jcgp/gui/settings/parameters/GUIIntegerParameter.java107
-rw-r--r--src/jcgp/gui/settings/parameters/GUIParameter.java235
-rw-r--r--src/jcgp/gui/settings/testcase/TestCaseTable.java124
25 files changed, 3579 insertions, 0 deletions
diff --git a/src/jcgp/gui/GUI.java b/src/jcgp/gui/GUI.java
new file mode 100644
index 0000000..79678bc
--- /dev/null
+++ b/src/jcgp/gui/GUI.java
@@ -0,0 +1,477 @@
+package jcgp.gui;
+
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.concurrent.Service;
+import javafx.concurrent.Task;
+import javafx.event.EventHandler;
+import javafx.scene.Scene;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.Pane;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+import jcgp.JCGP;
+import jcgp.backend.modules.problem.TestCaseProblem.TestCase;
+import jcgp.backend.resources.Resources;
+import jcgp.gui.console.ConsolePane;
+import jcgp.gui.dragresize.HorizontalDragResize;
+import jcgp.gui.dragresize.VerticalDragResize;
+import jcgp.gui.population.FunctionSelector;
+import jcgp.gui.population.GUINode;
+import jcgp.gui.population.PopulationPane;
+import jcgp.gui.settings.SettingsPane;
+
+/**
+ * Main class for the graphical user interface (GUI).
+ * <br><br>
+ * This class declares the main method used when running the GUI.
+ * In addition, all main GUI panes are declared and instantiated here.
+ * <br><br>
+ * The user interface is divided into 3 main components: the node grid
+ * ({@link PopulationPane}), the control pane ({@link SettingsPane}) and
+ * the console ({@link ConsolePane}). Click on any of the links in
+ * brackets to see more information about each interface component.
+ * <br><br>
+ * This class also contains the instance of JCGP responsible for
+ * running the experiments in GUI mode. JCGP's execution must be delegated
+ * to a separate thread so that the GUI remains unblocked. This is done using
+ * a JavaFX {@code Service} which calls {@code nextGeneration()} in a loop
+ * until it is interrupted by the main JavaFX thread.
+ * <br>
+ * This service also handles flushing the console in a thread safe way. This
+ * is done by synchronising the {@code nextGeneration()} and {@code flush()}
+ * method calls on a lock object.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public class GUI extends Application {
+
+ /*
+ * Actual GUI elements
+ */
+ private Stage stage;
+ private PopulationPane populationPane;
+ private ConsolePane console;
+ private SettingsPane settingsPane;
+ private final FunctionSelector functionSelector;
+
+ /*
+ * Flow control objects
+ */
+ private boolean running = false;
+ private final Object printLock = new Object();
+ private Service<Void> jcgpService;
+ private Runnable consoleFlush;
+
+ /*
+ * The experiment itself
+ */
+ private final JCGP jcgp;
+
+ public static Resources resources;
+
+
+ /**
+ * Start JCGP with the user interface.
+ *
+ * @param args no arguments are used.
+ */
+ public static void main(String[] args) {
+ // not much to do, simply launch the JavaFX application
+ launch();
+ }
+
+ /**
+ * Makes a new instance of GUI. This initialises the JCGP experiment and
+ * instantiates the function selector. It also creates the console flush task
+ * and the service responsible for running the JCGP experiment.
+ */
+ public GUI() {
+ jcgp = new JCGP();
+ resources = jcgp.getResources();
+ functionSelector = new FunctionSelector(jcgp.getResources().getFunctionSet());
+
+ /*
+ * This task flushes the console in a thread-safe way.
+ * The problem is that this task is executed using Platform.runLater()
+ * to ensure that the flush itself happens on the JavaFX thread. However,
+ * runLater() is not guaranteed to run anytime soon. If the time taken for
+ * jcgp to perform a single generation is shorter than the time taken for
+ * this task to be executed by the platform, consoleFlush tasks will be
+ * scheduled faster than they can be executed and the console will eventually
+ * freeze.
+ *
+ * This is addressed by synchronizing the flushes with each nextGeneration() call.
+ */
+ consoleFlush = new Runnable() {
+ @Override
+ public void run() {
+ /*
+ * Try to acquire printlock - wait here until jcgpService relinquishes it
+ * by calling wait(). This means that it is finished with the current generation
+ * and will wait for the console to be flushed to move on.
+ * It might be the case that the service has already released the lock by waiting
+ * on it; it makes no difference. In that case this will acquire the lock
+ * immediately and proceed to flush the console.
+ */
+ synchronized(printLock) {
+ /*
+ * The lock is acquired, at this point we are certain that jcgpService
+ * cannot execute; it is currently waiting to be notified about the lock.
+ * No additional consoleFlush tasks can be scheduled with runLater() because
+ * the service is waiting. We can now take our time to flush the console.
+ */
+ console.flush();
+ /*
+ * Once the console finishes flushing, we notify jcgpService to perform the
+ * next generation.
+ */
+ printLock.notifyAll();
+ }
+ }
+ };
+
+ /*
+ * This service runs on a separate thread and performs
+ * the experiment, including console prints, in a thread-safe
+ * way. It is synchronized with consoleFlush.
+ */
+ jcgpService = new Service<Void> () {
+ @Override
+ protected Task<Void> createTask() {
+ Task<Void> t = new Task<Void>() {
+ @Override
+ protected Void call() throws Exception {
+ /*
+ * Only execute if the experiment isn't finished
+ * and the service hasn't been cancelled.
+ */
+ while (!isCancelled() && !jcgp.isFinished()) {
+ /*
+ * Attempt to acquire the printlock.
+ * Successfully doing so means no printing
+ * is currently taking place and we are free
+ * to schedule a print task.
+ * This lock acquisition should never block. It should
+ * not be possible to execute this statement without
+ * having been notified by consoleFlush.
+ */
+ synchronized (printLock) {
+ /*
+ * Lock has been acquired, schedule a print
+ * task ahead of time. The actual print messages
+ * haven't been send to the console yet, that happens
+ * during nextGeneration(), but since we have the lock
+ * consoleFlush() will sit and wait for us to release it
+ * whenever we are finished queueing prints.
+ */
+ Platform.runLater(consoleFlush);
+ /*
+ * Perform the actual generation. Here zero or more
+ * strings might be sent to the console buffer.
+ */
+ jcgp.nextGeneration();
+ /*
+ * The generation is complete, relinquish the lock.
+ * By this point chances are the platform is already trying
+ * to execute the consoleFlush task that we scheduled. If it
+ * hasn't already started though, it doesn't matter; we will
+ * wait for a notification on the lock, which will only come
+ * when printing is complete.
+ */
+ printLock.wait();
+ /*
+ * We have been notified. This means all buffered messages have
+ * been successfully flushed to the actual console control and
+ * we are now ready to perform another generation (or break out
+ * of the loop if the loop conditions are no longer met).
+ */
+ }
+ /*
+ * We no longer own the lock, but neither does consoleFlush.
+ * The synchrony cycle has returned to its initial state, and we
+ * are free to acquire the lock again.
+ */
+ }
+ /*
+ * Something happened to break the while loop -
+ * either the experiment finished or the user pressed
+ * pause.
+ */
+ if (jcgp.isFinished()) {
+ // the experiment has finished, switch to pause mode
+ Platform.runLater(new Runnable() {
+ @Override
+ public void run() {
+ runningMode(false);
+ }
+ });
+ }
+ return null;
+ }
+ };
+ return t;
+ }
+ };
+ }
+
+ @Override
+ public void start(Stage primaryStage) throws Exception {
+ /*
+ * This method gets called when the application launches. Once it
+ * returns, the application falls into the main loop which handles
+ * events, so all elements must be constructed here.
+ */
+
+ // make the console and set it so it is used for JCGP prints
+ console = new ConsolePane();
+ jcgp.setConsole(console);
+
+ // store reference to the stage
+ stage = primaryStage;
+
+ /*
+ * The experiment layer contains all of the experiment-related panes.
+ * The only element that sits higher than this is the function selector.
+ */
+ BorderPane experimentLayer = new BorderPane();
+ /*
+ * The left frame encapsulates the population pane and the console.
+ * It goes into the center position of the experiment layer, next to the settings pane.
+ */
+ BorderPane leftFrame = new BorderPane();
+
+ /*
+ * The population pane is a TabPane containing a tab for each chromosome.
+ */
+ populationPane = new PopulationPane(this);
+
+ /*
+ * The settings pane is a big class containing the entire control pane
+ */
+ settingsPane = new SettingsPane(this);
+
+ // make control pane and console resizable
+ HorizontalDragResize.makeDragResizable(settingsPane);
+ VerticalDragResize.makeDragResizable(console);
+ // prevent resizables from growing larger than the experiment layer
+ settingsPane.maxWidthProperty().bind(experimentLayer.widthProperty());
+ console.maxHeightProperty().bind(experimentLayer.heightProperty());
+
+ // put console and population pane in the main frame
+ leftFrame.setCenter(populationPane);
+ leftFrame.setBottom(console);
+
+ // set the main frame and the control pane in the experiment layer
+ experimentLayer.setCenter(leftFrame);
+ experimentLayer.setRight(settingsPane);
+
+ /*
+ * Now we deal with the stage.
+ */
+ primaryStage.setTitle("JCGP");
+
+ // this pane holds the entire scene, that is its sole job.
+ Pane sceneParent = new Pane();
+ // the experiment layer should fill the entire scene parent
+ experimentLayer.prefHeightProperty().bind(sceneParent.heightProperty());
+ experimentLayer.prefWidthProperty().bind(sceneParent.widthProperty());
+ // the function selector goes over the experiment layer so it doesn't get covered by other panes
+ sceneParent.getChildren().addAll(experimentLayer, functionSelector);
+
+ // set the scene, minimum sizes, show
+ primaryStage.setScene(new Scene(sceneParent));
+ primaryStage.setMinWidth(800);
+ primaryStage.setMinHeight(600);
+ primaryStage.show();
+
+ // when the main stage closes, close the test case table as well
+ primaryStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
+ @Override
+ public void handle(WindowEvent event) {
+ if (settingsPane.getTestCaseTable() != null) {
+ settingsPane.getTestCaseTable().close();
+ }
+ }
+ });
+ }
+
+ /**
+ * Run/pause method.
+ * Run the experiment if it is paused, or pause it if it is running.
+ * <br>
+ * This method is the callback used by the run/pause button. It
+ * controls the jcgp service.
+ */
+ public void runPause() {
+ // do nothing if experiment is finished or parameters aren't valid
+ if (!jcgp.isFinished() && settingsPane.areParametersValid()) {
+ if (!running) {
+ runningMode(true);
+ jcgpService.restart();
+ } else {
+ jcgpService.cancel();
+ runningMode(false);
+ }
+ }
+ }
+
+ /**
+ * Perform a single generation using {@code nextGeneration()}.
+ * <br>
+ * On top of that, this method performs all of the housekeeping
+ * that is normally done before and after running, such as
+ * refreshing the chromosome panes.
+ */
+ public void step() {
+ // do nothing if experiment is finished, running or parameters aren't valid
+ if (!running && !jcgp.isFinished() && settingsPane.areParametersValid()) {
+ if (settingsPane.isResetRequired()) {
+ reset();
+ }
+ jcgp.nextGeneration();
+ console.flush();
+
+ populationPane.updateGenes();
+ settingsPane.revalidateParameters();
+ settingsPane.updateControls(false, jcgp.isFinished());
+ }
+ }
+
+ /**
+ * Reset button callback. If the parameters are valid,
+ * this resets the entire experiment by calling {@code reset()}
+ * on jcgp.
+ */
+ public void reset() {
+ if (!running && settingsPane.areParametersValid()) {
+ setEvaluating(false);
+ jcgp.reset();
+ settingsPane.applyParameters();
+ reDraw();
+ }
+ }
+
+ /**
+ * Does a complete GUI refresh.
+ * This is potentially lengthy, so use with care.
+ */
+ public void reDraw() {
+ populationPane.remakeTabs();
+ settingsPane.revalidateParameters();
+ settingsPane.updateControls(false, jcgp.isFinished());
+ console.flush();
+ }
+
+ /**
+ * Toggles the entire GUI between run and pause
+ * mode.
+ * <br><br>
+ * A lot of the GUI must be enabled or disabled
+ * depending on what the experiment is doing. This
+ * method provides a one-line way to make
+ * all required adjustments.
+ *
+ * @param value true if experiment is running, false otherwise.
+ */
+ private void runningMode(boolean value) {
+ if (value) {
+ if (settingsPane.isResetRequired()) {
+ reset();
+ }
+ } else {
+ populationPane.updateGenes();
+ settingsPane.revalidateParameters();
+ }
+ populationPane.setDisable(value);
+ settingsPane.updateControls(value, jcgp.isFinished());
+
+ running = value;
+ }
+
+ /**
+ * Refresh the function selector, used when functions are enabled or disabled.
+ */
+ public void updateFunctionSelector() {
+ functionSelector.remakeFunctions(jcgp.getResources().getFunctionSet());
+ }
+
+ /**
+ * @return true if jcgp is evolving.
+ */
+ public boolean isWorking() {
+ return running;
+ }
+
+ /**
+ * Relocate the function selector to the right position
+ * relative to the specified node and set it visible.
+ *
+ * @param event the mouse event containing cursor coordinates.
+ * @param node the node whose function should be changed.
+ */
+ public void bringFunctionSelector(MouseEvent event, GUINode node) {
+ functionSelector.relocateAndShow(event, node);
+ }
+
+ /**
+ * @return a reference to the {@code JCGP} experiment.
+ */
+ public JCGP getExperiment() {
+ return jcgp;
+ }
+
+ /**
+ * Starts the evaluation process with the given test case.
+ * It does so by calling {@code evaluateTestCase()} on
+ * the population pane.
+ *
+ * @param testCase the test case to evaluate.
+ */
+ public void evaluateTestCase(TestCase<Object> testCase) {
+ populationPane.evaluateTestCase(testCase);
+ }
+
+ /**
+ * Hide all evaluated values. This should be called when
+ * evaluations are no longer being performed.
+ */
+ public void hideGeneValues() {
+ populationPane.hideValues();
+ }
+
+ /**
+ * Set the system into evaluation mode.
+ * When in evaluation mode, the population pane
+ * refreshes the node values whenever connection
+ * changes happen.
+ *
+ * @param value true if evaluations are happening, false otherwise.
+ */
+ public void setEvaluating(boolean value) {
+ populationPane.setEvaluating(value);
+ }
+
+ /**
+ * @return a reference to the GUI stage.
+ */
+ public Stage getStage() {
+ return stage;
+ }
+
+ /**
+ * Writes all buffered content out to the GUI console.
+ */
+ public void flushConsole() {
+ console.flush();
+ }
+
+ /**
+ * @return the index of the chromosome currently being looked at.
+ */
+ public int getChromosomeIndex() {
+ return populationPane.getSelectionModel().getSelectedIndex();
+ }
+}
diff --git a/src/jcgp/gui/console/ConsolePane.java b/src/jcgp/gui/console/ConsolePane.java
new file mode 100644
index 0000000..de193a5
--- /dev/null
+++ b/src/jcgp/gui/console/ConsolePane.java
@@ -0,0 +1,122 @@
+package jcgp.gui.console;
+
+import javafx.event.ActionEvent;
+import javafx.event.Event;
+import javafx.event.EventDispatchChain;
+import javafx.event.EventDispatcher;
+import javafx.event.EventHandler;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.SeparatorMenuItem;
+import javafx.scene.control.TextArea;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.AnchorPane;
+import jcgp.backend.resources.Console;
+import jcgp.gui.constants.Constants;
+
+/**
+ * Console pane used by the GUI to display CGP output messages.
+ * This class realises {@code Console}. It consists of a JavaFX
+ * {@code TextArea} and a {@code StringBuffer}. The buffer is filled
+ * as print messages are queued. Calling {@code flush()} writes the
+ * contents of the buffer to the {@code TextArea} and empties the buffer.
+ *
+ * @see Console
+ * @author Eduardo Pedroni
+ *
+ */
+public class ConsolePane extends AnchorPane implements Console {
+
+ private TextArea textArea = new TextArea("Welcome to JCGP!\n");
+ private StringBuffer printBuffer = new StringBuffer();
+
+ /**
+ * Creates a new instance of this class.
+ */
+ public ConsolePane() {
+ super();
+ textArea.setEditable(false);
+ /*
+ * This nasty hack is needed because the default TextArea ContextMenu is not
+ * in the public API, making it impossible to override it with a custom one.
+ * This has not been fixed as of 8/4/2014.
+ *
+ * The following code modifies the EventDispatcher to consume the right mouse
+ * button click, preventing the default menu from appearing. It propagates the mouse
+ * click further so other elements will respond appropriately.
+ *
+ * TODO this should be refactored once the API is updated.
+ */
+ final EventDispatcher initial = textArea.getEventDispatcher();
+ textArea.setEventDispatcher(new EventDispatcher() {
+ @Override
+ public Event dispatchEvent(Event event, EventDispatchChain tail) {
+ if (event instanceof MouseEvent) {
+ MouseEvent mouseEvent = (MouseEvent)event;
+ if (mouseEvent.getButton() == MouseButton.SECONDARY ||
+ (mouseEvent.getButton() == MouseButton.PRIMARY && mouseEvent.isControlDown())) {
+ event.consume();
+ }
+ }
+ return initial.dispatchEvent(event, tail);
+ }
+ });
+
+ // make the new context menu including the clear option
+ MenuItem copySelected = new MenuItem("Copy");
+ copySelected.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ textArea.copy();
+ }
+ });
+ MenuItem selectAll = new MenuItem("Select all");
+ selectAll.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ textArea.selectAll();
+ }
+ });
+ MenuItem clearConsole = new MenuItem("Clear");
+ clearConsole.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ textArea.setText("");
+ }
+ });
+
+ textArea.setContextMenu(new ContextMenu(copySelected,
+ selectAll,
+ new SeparatorMenuItem(),
+ clearConsole));
+
+ // anchor the text area so it resizes automatically
+ AnchorPane.setTopAnchor(textArea, Constants.RESIZE_MARGIN);
+ AnchorPane.setBottomAnchor(textArea, 0.0);
+ AnchorPane.setRightAnchor(textArea, 0.0);
+ AnchorPane.setLeftAnchor(textArea, 0.0);
+
+ setMinHeight(Constants.CONSOLE_MIN_HEIGHT);
+ setPrefHeight(Constants.CONSOLE_MIN_HEIGHT);
+
+ getChildren().add(textArea);
+ }
+
+ @Override
+ public void println(String s) {
+ printBuffer.append(s + "\n");
+ }
+
+ @Override
+ public void print(String s) {
+ printBuffer.append(s);
+ }
+
+ @Override
+ public void flush() {
+ textArea.appendText(printBuffer.toString());
+ printBuffer = new StringBuffer();
+ }
+
+}
diff --git a/src/jcgp/gui/constants/Constants.java b/src/jcgp/gui/constants/Constants.java
new file mode 100644
index 0000000..509d982
--- /dev/null
+++ b/src/jcgp/gui/constants/Constants.java
@@ -0,0 +1,170 @@
+package jcgp.gui.constants;
+
+import javafx.scene.paint.Paint;
+
+/**
+ * Holds the constants used in the GUI.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public final class Constants {
+
+ /**
+ * Private constructor to prevent instantiation.
+ */
+ private Constants(){}
+
+ /*---------------------------------------------------------------------------------------------------
+ * Colour Strings
+ *-------------------------------------------------------------------------------------------------*/
+ /**
+ * A {@code String} containing the colour used for representing neutrality.
+ */
+ public static final String NEUTRAL_COLOUR = "#FFFFFF";
+ /**
+ * A {@code String} containing the hexadecimal colour used for representing a hard highlight.
+ * A "hard" select, for instance, happens when an output path is locked on the chromosome pane.
+ */
+ public static final String HARD_HIGHLIGHT_COLOUR = "#5496FF";
+ /**
+ * A {@code String} containing the hexadecimal colour used for a medium highlight.
+ * One example of such a selection is the colour applied to a node when it is hovered over.
+ */
+ public static final String MEDIUM_HIGHLIGHT_COLOUR = "#75BAFF";
+ /**
+ * A {@code String} containing the hexadecimal colour used for a soft highlight.
+ * When hovering over a node, its connections are soft-selected.
+ */
+ public static final String SOFT_HIGHLIGHT_COLOUR = "#C7DFFF";
+ /**
+ * A {@code String} containing the hexadecimal colour used for representing a good selection.
+ * Ideally a shade of green, used for instance when a manual connection is valid.
+ */
+ public static final String GOOD_SELECTION_COLOUR = "#38C25B";
+ /**
+ * A {@code String} containing the hexadecimal colour used for representing a neutral selection.
+ * Ideally a shade of yellow, used for instance when a manual connection is already the current connection.
+ */
+ public static final String NEUTRAL_SELECTION_COLOUR = "#FFEF73";
+ /**
+ * A {@code String} containing the hexadecimal colour used for representing a bad selection.
+ * Ideally a shade of red, use for instance when a manual connection is not valid.
+ */
+ public static final String BAD_SELECTION_COLOUR = "#FF5C5C";
+ /**
+ * A {@code String} containing the hexadecimal colour used for the gene sockets.
+ */
+ public static final String SOCKET_COLOUR = "#FFFFFF";
+
+ /*---------------------------------------------------------------------------------------------------
+ * Colour Paints
+ *-------------------------------------------------------------------------------------------------*/
+ /**
+ * A {@code Paint} containing the colour used for representing neutrality.
+ */
+ public static final Paint NEUTRAL_PAINT = Paint.valueOf(NEUTRAL_COLOUR);
+ /**
+ * A {@code Paint} containing the colour used for representing a hard highlight.
+ * A "hard" select, for instance, happens when an output path is locked on the chromosome pane.
+ */
+ public static final Paint HARD_HIGHLIGHT_PAINT = Paint.valueOf(HARD_HIGHLIGHT_COLOUR);
+ /**
+ * A {@code Paint} containing the colour used for a medium highlight.
+ * One example of such a selection is the colour applied to a node when it is hovered over.
+ */
+ public static final Paint MEDIUM_HIGHLIGHT_PAINT = Paint.valueOf(MEDIUM_HIGHLIGHT_COLOUR);
+ /**
+ * A {@code Paint} containing the colour used for a soft highlight.
+ * When hovering over a node, its connections are soft-selected.
+ */
+ public static final Paint SOFT_HIGHLIGHT_PAINT = Paint.valueOf(SOFT_HIGHLIGHT_COLOUR);
+ /**
+ * A {@code Paint} containing the colour used for representing a good selection.
+ * Ideally a shade of green, used for instance when a manual connection is valid.
+ */
+ public static final Paint GOOD_SELECTION_PAINT = Paint.valueOf(GOOD_SELECTION_COLOUR);
+ /**
+ * A {@code Paint} containing the colour used for representing a neutral selection.
+ * Ideally a shade of yellow, used for instance when a manual connection is already the current connection.
+ */
+ public static final Paint NEUTRAL_SELECTION_PAINT = Paint.valueOf(NEUTRAL_SELECTION_COLOUR);
+ /**
+ * A {@code Paint} containing the colour used for representing a bad selection.
+ * Ideally a shade of red, use for instance when a manual connection is not valid.
+ */
+ public static final Paint BAD_SELECTION_PAINT = Paint.valueOf(BAD_SELECTION_COLOUR);
+ /**
+ * A {@code Paint} containing the colour used for the gene sockets.
+ */
+ public static final Paint SOCKET_PAINT = Paint.valueOf(SOCKET_COLOUR);
+
+ /*---------------------------------------------------------------------------------------------------
+ * Sizes and distances
+ *-------------------------------------------------------------------------------------------------*/
+ /**
+ * The width or height of the area that can be clicked on
+ * to drag-resize a pane.
+ */
+ public static final double RESIZE_MARGIN = 5.0;
+ /**
+ * The minimum width of the settings pane, to prevent it
+ * from being resized beyond visibility.
+ */
+ public static final double SETTINGS_MIN_WIDTH = 200;
+ /**
+ * The minimum width of the console pane, to prevent it
+ * from being resized beyond visibility.
+ */
+ public static final double CONSOLE_MIN_HEIGHT = 100;
+ /**
+ * Radius used for the representation of nodes in the grid.
+ */
+ public static final double NODE_RADIUS = 35;
+ /**
+ * Spacing between each node.
+ */
+ public static final double SPACING = 15;
+ /**
+ * The margin between the genes and the edge of the chromosome pane.
+ */
+ public static final double CHROMOSOME_PANE_MARGIN = 10;
+ /**
+ * The angle across which the node's sockets are evenly distributed.
+ */
+ public static final double THETA = Math.PI / 1.4;
+ /**
+ * The radius of the connection sockets, calculated as a function of NODE_RADIUS.
+ */
+ public static final double SOCKET_RADIUS = Math.sqrt(NODE_RADIUS) / 1.8;
+ /**
+ * Size of the text in each node.
+ */
+ public static final double NODE_TEXT = NODE_RADIUS / 2.5;
+
+ /*---------------------------------------------------------------------------------------------------
+ * CSS Styles
+ * TODO extract to stylesheet?
+ *-------------------------------------------------------------------------------------------------*/
+ /**
+ * The basic style of text boxes used in parameters.
+ */
+ public static final String BASE_TEXT_STYLE = "-fx-border-color: #C9C9C9; -fx-border-radius: 2; -fx-padding: 0; ";
+ /**
+ * The basic style of check boxes used in parameters.
+ */
+ public static final String BASE_CHECKBOX_STYLE = "-fx-padding: 0; ";
+ /**
+ * The style applied to invalid parameters, using BAD_SELECTION_COLOUR.
+ */
+ public static final String INVALID_PARAMETER_STYLE = "-fx-background-color: " + BAD_SELECTION_COLOUR;
+ /**
+ * The style applied to neutral parameters, using NEUTRAL_SELECTION_COLOUR.
+ */
+ public static final String WARNING_PARAMETER_STYLE = "-fx-background-color: " + NEUTRAL_SELECTION_COLOUR;
+ /**
+ * The style applied to valid parameters, using NEUTRAL_COLOUR.
+ */
+ public static final String VALID_PARAMETER_STYLE = "-fx-background-color: " + NEUTRAL_COLOUR;
+
+}
diff --git a/src/jcgp/gui/constants/Position.java b/src/jcgp/gui/constants/Position.java
new file mode 100644
index 0000000..6d4e02b
--- /dev/null
+++ b/src/jcgp/gui/constants/Position.java
@@ -0,0 +1,103 @@
+package jcgp.gui.constants;
+
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Line;
+import jcgp.gui.GUI;
+import jcgp.gui.population.GUIGene;
+import jcgp.gui.population.GUIInput;
+import jcgp.gui.population.GUINode;
+import jcgp.gui.population.GUIOutput;
+
+/**
+ * Abstracts the task of positioning GUI components.
+ * <br>
+ * Do not instantiate this class; instead, use the {@code public static} methods provided.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public final class Position {
+
+ /**
+ * Private constructor to prevent instantiation.
+ */
+ private Position() {}
+
+ /**
+ * Sets the X and Y layouts of the specified input to the appropriate values, according to its index.
+ *
+ * @param input the {@code GUIInput} instance to relocate.
+ */
+ public static void place(GUIInput input) {
+ // inputs are the first column, so we only worry about the margin and their index
+ input.relocate(Constants.CHROMOSOME_PANE_MARGIN,
+ input.getInput().getIndex() * (2 * Constants.NODE_RADIUS + Constants.SPACING) + Constants.CHROMOSOME_PANE_MARGIN);
+ }
+
+ /**
+ * Sets the X and Y layouts of the specified node to the appropriate values, according to its row and column values.
+ * This also connects the start of every line with its respective socket. Therefore, this method should be called at least
+ * once when the {@code GUINode} is instantiated.
+ *
+ * @param node the {@code GUINode} instance to relocate.
+ */
+ public static void place(GUINode node) {
+ // calculate x and y offsets, in relation to the layout origin
+ double xOffset = (node.getNode().getColumn() + 1) * (2 * Constants.NODE_RADIUS + Constants.SPACING) + Constants.CHROMOSOME_PANE_MARGIN;
+ double yOffset = node.getNode().getRow() * (2 * Constants.NODE_RADIUS + Constants.SPACING) + Constants.CHROMOSOME_PANE_MARGIN;
+
+ // move node
+ node.relocate(xOffset, yOffset);
+
+ // use the offset and the socket positions to connect the lines
+ for (int i = 0; i < GUI.resources.arity(); i++) {
+ node.getLines()[i].setStartX(node.getSocket(i).getCenterX() + xOffset + Constants.NODE_RADIUS + Constants.SOCKET_RADIUS);
+ node.getLines()[i].setStartY(node.getSocket(i).getCenterY() + yOffset + Constants.NODE_RADIUS);
+ }
+ }
+
+ /**
+ * Sets the X and Y layouts of the specified output to the appropriate values, according to its index.
+ * This also connects the start of the output's single line to its single input socket.Therefore,
+ * this method should be called at least once when the {@code GUIOutput} is instantiated.
+ *
+ * @param output the {@code GUIOutput} instance to relocate.
+ */
+ public static void place(GUIOutput output) {
+ // the output's position is a function of the number of columns and its own index
+ output.relocate(((GUI.resources.columns() + 1) * (2 * Constants.NODE_RADIUS + Constants.SPACING)) + Constants.CHROMOSOME_PANE_MARGIN,
+ output.getOutput().getIndex() * (2 * Constants.NODE_RADIUS + Constants.SPACING) + Constants.CHROMOSOME_PANE_MARGIN);
+ output.getLines()[0].setStartX(output.getLayoutX() - Constants.NODE_RADIUS);
+ output.getLines()[0].setStartY(output.getLayoutY());
+ }
+
+ /**
+ * Connects the end of a specified line to the specified gene.
+ *
+ * @param line the line to connect.
+ * @param target the target gene to connect to.
+ */
+ public static void connect(Line line, GUIGene target) {
+ // set line ends based on the layout position of the target
+ line.setEndX(target.getLayoutX() + Constants.NODE_RADIUS);
+ line.setEndY(target.getLayoutY());
+ }
+
+ /**
+ * Relocates the given socket to the appropriate position given the
+ * socket's index.
+ *
+ * @param index the socket index.
+ * @param socket the {@code Circle} instance to relocate.
+ */
+ public static void placeSocket(int index, Circle socket) {
+ // calculate the angle with respect to the x-axis
+ double angle = (((index + 1) / ((double) (GUI.resources.arity() + 1))) * Constants.THETA) - (Constants.THETA / 2);
+ // convert to cartesian form
+ double xPos = -Math.cos(angle) * Constants.NODE_RADIUS;
+ double yPos = Math.sin(angle) * Constants.NODE_RADIUS;
+ // set centre
+ socket.setCenterX(xPos);
+ socket.setCenterY(yPos);
+ }
+}
diff --git a/src/jcgp/gui/dragresize/HorizontalDragResize.java b/src/jcgp/gui/dragresize/HorizontalDragResize.java
new file mode 100644
index 0000000..e88eafd
--- /dev/null
+++ b/src/jcgp/gui/dragresize/HorizontalDragResize.java
@@ -0,0 +1,131 @@
+package jcgp.gui.dragresize;
+
+import javafx.event.EventHandler;
+import javafx.scene.Cursor;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
+import jcgp.gui.constants.Constants;
+
+/**
+ * This class adds horizontal drag resize functionality to any
+ * arbitrary region provided. This is done by using the static
+ * method {@code makeDragResizable()}.
+ * <br><br>
+ * This is based on a class by Andrew Till found on:
+ * http://andrewtill.blogspot.co.uk/2012/12/dragging-to-resize-javafx-region.html
+ *
+ */
+public class HorizontalDragResize {
+
+ private boolean dragging = false;
+ private final Region region;
+
+ /**
+ * For internal use only, creates an instance of the actual
+ * resizer used.
+ *
+ * @param region the region to make resizable.
+ */
+ private HorizontalDragResize(Region region) {
+ this.region = region;
+ }
+
+ /**
+ * Makes the specified region drag resizable.
+ * This particular implementation only creates a resize
+ * click-and-drag area on the left side of the region.
+ * The resize area is defined by {@code GUI.RESIZE_MARGIN}.
+ *
+ * @param region the region to make resizable.
+ */
+ public static void makeDragResizable(final Region region) {
+ // make the instance, this actually performs the resizing
+ final HorizontalDragResize dr = new HorizontalDragResize(region);
+
+ // set mouse listeners
+ region.setOnMousePressed(new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ dr.mousePressed(event);
+ }
+ });
+ region.setOnMouseDragged(new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ dr.mouseDragged(event);
+ }
+ });
+ region.setOnMouseMoved(new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ dr.mouseMoved(event);
+ }
+ });
+ region.setOnMouseReleased(new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ dr.mouseReleased();
+ }
+ });
+
+ }
+
+ /**
+ * If the press happened in the resize area, raise the drag flag.
+ *
+ * @param event the associated mouse event.
+ */
+ private void mousePressed(MouseEvent event) {
+ if(isInDraggableZone(event)) {
+ dragging = true;
+ }
+ }
+
+ /**
+ * If drag flag is high, resize the region to match the mouse position.
+ *
+ * @param event the associated mouse event.
+ */
+ private void mouseDragged(MouseEvent event) {
+ if(dragging) {
+ double newWidth = region.getWidth() - event.getX();
+ if (newWidth >= region.getMinWidth()) {
+ region.setPrefWidth(newWidth);
+ } else {
+ region.setPrefWidth(region.getMinWidth());
+ }
+ }
+ }
+
+ /**
+ * Change the cursor if the mouse position overlaps with the resize area.
+ *
+ * @param event the associated mouse event.
+ */
+ private void mouseMoved(MouseEvent event) {
+ if(isInDraggableZone(event) || dragging) {
+ region.setCursor(Cursor.H_RESIZE);
+ } else {
+ region.setCursor(Cursor.DEFAULT);
+ }
+ }
+
+ /**
+ * Finish resizing.
+ */
+ private void mouseReleased() {
+ dragging = false;
+ region.setCursor(Cursor.DEFAULT);
+ }
+
+ /**
+ * Assert whether the mouse cursor is in the draggable area defined by {@code GUI.RESIZE_MARGIN}.
+ *
+ * @param event the associated mouse event.
+ * @return true if the mouse position is in the draggable area.
+ */
+ private boolean isInDraggableZone(MouseEvent event) {
+ return event.getX() < (Constants.RESIZE_MARGIN);
+ }
+
+}
diff --git a/src/jcgp/gui/dragresize/VerticalDragResize.java b/src/jcgp/gui/dragresize/VerticalDragResize.java
new file mode 100644
index 0000000..4f784e5
--- /dev/null
+++ b/src/jcgp/gui/dragresize/VerticalDragResize.java
@@ -0,0 +1,132 @@
+package jcgp.gui.dragresize;
+
+import javafx.event.EventHandler;
+import javafx.scene.Cursor;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
+import jcgp.gui.constants.Constants;
+
+/**
+ * This class adds vertical drag resize functionality to any
+ * arbitrary region provided. This is done by using the static
+ * method {@code makeDragResizable()}.
+ * <br><br>
+ * This is based on a class by Andrew Till found on:
+ * http://andrewtill.blogspot.co.uk/2012/12/dragging-to-resize-javafx-region.html
+ *
+ */
+public class VerticalDragResize {
+
+ private boolean dragging = false;
+ private final Region region;
+
+ /**
+ * For internal use only, creates an instance of the actual
+ * resizer used.
+ *
+ * @param region the region to make resizable.
+ */
+ private VerticalDragResize(Region region) {
+ this.region = region;
+ }
+
+ /**
+ * Makes the specified region drag resizable.
+ * This particular implementation only creates a resize
+ * click-and-drag area on the top side of the region.
+ * The resize area is defined by {@code GUI.RESIZE_MARGIN}.
+ *
+ * @param region the region to make resizable.
+ */
+ public static void makeDragResizable(final Region region) {
+ // make the instance, this actually performs the resizing
+ final VerticalDragResize dr = new VerticalDragResize(region);
+
+ // set mouse listeners
+ region.setOnMousePressed(new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ dr.mousePressed(event);
+ }
+ });
+ region.setOnMouseDragged(new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ dr.mouseDragged(event);
+ }
+ });
+ region.setOnMouseMoved(new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ dr.mouseMoved(event);
+ }
+ });
+ region.setOnMouseReleased(new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ dr.mouseReleased();
+ }
+ });
+
+ }
+
+ /**
+ * If the press happened in the resize area, raise the drag flag.
+ *
+ * @param event the associated mouse event.
+ */
+ private void mousePressed(MouseEvent event) {
+ if(isInDraggableZone(event)) {
+ dragging = true;
+ }
+ }
+
+ /**
+ * If drag flag is high, resize the region to match the mouse position.
+ *
+ * @param event the associated mouse event.
+ */
+ private void mouseDragged(MouseEvent event) {
+ if(dragging) {
+ double newHeight = region.getHeight() - event.getY();
+ if (newHeight >= region.getMinHeight()) {
+ region.setPrefHeight(newHeight);
+ } else {
+ region.setPrefHeight(region.getMinHeight());
+ }
+ }
+ }
+
+ /**
+ * Change the cursor if the mouse position overlaps with the resize area.
+ *
+ * @param event the associated mouse event.
+ */
+ private void mouseMoved(MouseEvent event) {
+ if(isInDraggableZone(event) || dragging) {
+ region.setCursor(Cursor.V_RESIZE);
+ }
+ else {
+ region.setCursor(Cursor.DEFAULT);
+ }
+ }
+
+ /**
+ * Finish resizing.
+ */
+ private void mouseReleased() {
+ dragging = false;
+ region.setCursor(Cursor.DEFAULT);
+ }
+
+ /**
+ * Assert whether the mouse cursor is in the draggable area defined by {@code GUI.RESIZE_MARGIN}.
+ *
+ * @param event the associated mouse event.
+ * @return true if the mouse position is in the draggable area.
+ */
+ private boolean isInDraggableZone(MouseEvent event) {
+ return event.getY() < (Constants.RESIZE_MARGIN);
+ }
+
+}
diff --git a/src/jcgp/gui/handlers/InputHandlers.java b/src/jcgp/gui/handlers/InputHandlers.java
new file mode 100644
index 0000000..cc677eb
--- /dev/null
+++ b/src/jcgp/gui/handlers/InputHandlers.java
@@ -0,0 +1,56 @@
+package jcgp.gui.handlers;
+
+import javafx.event.EventHandler;
+import javafx.scene.input.MouseEvent;
+import jcgp.gui.population.GUIGene.GUIGeneState;
+import jcgp.gui.population.GUIInput;
+
+/**
+ * Holds the handlers that define the behaviour of {@code GUIInput}.
+ * <br><br>
+ * The handlers are instantiated here statically and added to {@code GUIInput}
+ * instances using {@code InputHandlers.addHandlers(...)}. This guarantees that
+ * all inputs behave the same way without instantiating a new set of handlers for
+ * each input instance.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public final class InputHandlers {
+
+ /**
+ * Private constructor to prevent instantiation.
+ */
+ private InputHandlers() {}
+
+ /**
+ * Inputs don't do much; set state to hover when mouse enters.
+ */
+ private static EventHandler<MouseEvent> mouseEnteredHandler = new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ ((GUIInput) event.getSource()).setState(GUIGeneState.HOVER);
+ }
+ };
+
+ /**
+ * Inputs don't do much; set state to neutral when mouse exits.
+ */
+ private static EventHandler<MouseEvent> mouseExitedHandler = new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ ((GUIInput) event.getSource()).setState(GUIGeneState.NEUTRAL);
+ }
+ };
+
+ /**
+ * Adds all handlers to the specified input.
+ *
+ * @param input the {@code GUIInput} to which the handlers will be added.
+ */
+ public static void addHandlers(GUIInput input) {
+ input.addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredHandler);
+ input.addEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedHandler);
+ }
+
+}
diff --git a/src/jcgp/gui/handlers/NodeHandlers.java b/src/jcgp/gui/handlers/NodeHandlers.java
new file mode 100644
index 0000000..b413a62
--- /dev/null
+++ b/src/jcgp/gui/handlers/NodeHandlers.java
@@ -0,0 +1,164 @@
+package jcgp.gui.handlers;
+
+import javafx.event.EventHandler;
+import javafx.scene.input.MouseDragEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Line;
+import jcgp.backend.population.Gene;
+import jcgp.gui.GUI;
+import jcgp.gui.constants.Position;
+import jcgp.gui.population.ChromosomePane;
+import jcgp.gui.population.GUIConnection;
+import jcgp.gui.population.GUIGene;
+import jcgp.gui.population.GUIGene.GUIGeneState;
+import jcgp.gui.population.GUINode;
+
+/**
+ * Holds the handlers that define the behaviour of {@code GUINode}.
+ * <br><br>
+ * The handlers are instantiated here statically and added to {@code GUINode}
+ * instances using {@code NodeHandlers.addHandlers(...)}. This guarantees that
+ * all nodes behave the same way without instantiating a new set of handlers for
+ * each node instance.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public final class NodeHandlers {
+
+ /**
+ * Private constructor to prevent instantiation.
+ */
+ private NodeHandlers() {}
+
+ /**
+ * Set the node to {@code GUIGeneState.HOVER} state, and set its immediate connections to {@code GUIGeneState.EXTENDED_HOVER}.
+ */
+ private static EventHandler<MouseEvent> mouseEnteredHandler = new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ // acquire the source, we can safely cast it to GUINode
+ GUINode source = (GUINode) event.getSource();
+
+ source.setState(GUIGeneState.HOVER);
+ for (int i = 0; i < GUI.resources.arity(); i++) {
+ ((GUIGene) ((Gene) source.getNode().getConnection(i)).getGUIObject()).setState(GUIGeneState.EXTENDED_HOVER);
+ }
+ }
+ };
+
+ /**
+ * Set the node and its immediate connections to {@code GUIGeneState.NEUTRAL} state.
+ */
+ private static EventHandler<MouseEvent> mouseExitedHandler = new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ // acquire the source, we can safely cast it to GUINode
+ GUINode source = (GUINode) event.getSource();
+
+ if (Target.getSourceMutable() != source) {
+ source.setState(GUIGeneState.NEUTRAL);
+ for (int i = 0; i < GUI.resources.arity(); i++) {
+ ((GUIGene) ((Gene) source.getNode().getConnection(i)).getGUIObject()).setState(GUIGeneState.NEUTRAL);
+ }
+ }
+ }
+ };
+
+ private static EventHandler<MouseEvent> socketDragDetected = new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ // it's safe to assume that the source is the socket
+ ((GUINode) ((Circle) event.getSource()).getParent()).startFullDrag();
+ }
+ };
+
+ private static EventHandler<MouseEvent> socketMousePressedHandler = new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ // it's safe to assume that the source is the socket
+ Target.start((Circle) event.getSource());
+ }
+ };
+
+ private static EventHandler<MouseEvent> socketMouseDraggedHandler = new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ // this can only happen after a press, so we know Target is up-to-date
+ if (!Target.isProspecting()) {
+ GUINode node = (GUINode) Target.getSourceMutable();
+ Line line = Target.getConnectionLine();
+ line.setEndX(event.getX() + node.getLayoutX());
+ line.setEndY(event.getY() + node.getLayoutY());
+ }
+
+ }
+ };
+
+ private static EventHandler<MouseEvent> socketMouseReleasedHandler = new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+
+ GUINode node = (GUINode) ((Circle) event.getSource()).getParent();
+ int connectionId = Integer.valueOf(((Circle) event.getSource()).getId());
+
+ Position.connect(node.getLines()[connectionId], (GUIGene) ((Gene) node.getNode().getConnection(connectionId)).getGUIObject());
+ }
+ };
+
+ private static EventHandler<MouseDragEvent> dragEnteredHandler = new EventHandler<MouseDragEvent>() {
+ @Override
+ public void handle(MouseDragEvent event) {
+ // acquire the source, we can safely cast it to GUINode
+ GUINode source = (GUINode) event.getSource();
+ if (Target.getCurrentConnection() == source) {
+ source.setState(GUIGeneState.NEUTRAL_TARGET);
+ // we are now prospecting
+ Target.setProspecting(true);
+ Position.connect(Target.getConnectionLine(), source);
+ } else if (ChromosomePane.isAllowed(Target.getSourceMutable(), (GUIConnection) source)) {
+ source.setState(GUIGeneState.GOOD_TARGET);
+ // we are now prospecting
+ Target.setProspecting(true);
+ Position.connect(Target.getConnectionLine(), source);
+ } else {
+ source.setState(GUIGeneState.BAD_TARGET);
+ }
+ }
+ };
+
+ private static EventHandler<MouseDragEvent> dragExitedHandler = new EventHandler<MouseDragEvent>() {
+ @Override
+ public void handle(MouseDragEvent event) {
+ // acquire the source, we can safely cast it to GUINode
+ GUINode source = (GUINode) event.getSource();
+ source.setState(GUIGeneState.NEUTRAL);
+
+ // no longer prospecting
+ Target.setProspecting(false);
+ }
+ };
+
+ /**
+ * Adds all handlers to the specified node.
+ *
+ * @param node the {@code GUINode} to which the handlers will be added.
+ */
+ public static void addHandlers(GUINode node) {
+ node.addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredHandler);
+ node.addEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedHandler);
+
+ node.addEventHandler(MouseDragEvent.MOUSE_DRAG_ENTERED, dragEnteredHandler);
+ node.addEventHandler(MouseDragEvent.MOUSE_DRAG_EXITED, dragExitedHandler);
+
+ Circle[] sockets = node.getSockets();
+ for (int s = 0; s < sockets.length; s++) {
+
+ sockets[s].addEventFilter(MouseEvent.DRAG_DETECTED, socketDragDetected);
+ sockets[s].addEventHandler(MouseEvent.MOUSE_PRESSED, socketMousePressedHandler);
+ sockets[s].addEventHandler(MouseEvent.MOUSE_DRAGGED, socketMouseDraggedHandler);
+ sockets[s].addEventHandler(MouseEvent.MOUSE_RELEASED, socketMouseReleasedHandler);
+ }
+ }
+}
diff --git a/src/jcgp/gui/handlers/OutputHandlers.java b/src/jcgp/gui/handlers/OutputHandlers.java
new file mode 100644
index 0000000..b89d746
--- /dev/null
+++ b/src/jcgp/gui/handlers/OutputHandlers.java
@@ -0,0 +1,84 @@
+package jcgp.gui.handlers;
+
+import javafx.event.EventHandler;
+import javafx.scene.input.MouseEvent;
+import jcgp.backend.population.Gene;
+import jcgp.gui.population.GUIConnection;
+import jcgp.gui.population.GUIGene.GUIGeneState;
+import jcgp.gui.population.GUIOutput;
+
+/**
+ * Holds the handlers that define the behaviour of {@code GUIOutput}.
+ * <br><br>
+ * The handlers are instantiated here statically and added to {@code GUIOutput}
+ * instances using {@code OutputHandlers.addHandlers(...)}. This guarantees that
+ * all outputs behave the same way without instantiating a new set of handlers for
+ * each output instance.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public final class OutputHandlers {
+
+ /**
+ * Private constructor to prevent instantiation.
+ */
+ private OutputHandlers() {}
+
+ /**
+ * Set the output to {@code GUIGeneState.HOVER} state, and recursively set its active genes
+ * to {@code GUIGeneState.ACTIVE_HOVER}.
+ */
+ private static EventHandler<MouseEvent> mouseEnteredHandler = new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ // acquire the source, we can safely cast it to GUIOutput
+ GUIOutput source = (GUIOutput) event.getSource();
+
+ source.setState(GUIGeneState.HOVER);
+ ((GUIConnection) ((Gene) source.getOutput().getSource()).getGUIObject()).setStateRecursively(GUIGeneState.ACTIVE_HOVER);
+ }
+ };
+
+ /**
+ * Set the output and all of its active genes to {@code GUIGeneState.NEUTRAL} state.
+ */
+ private static EventHandler<MouseEvent> mouseExitedHandler = new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ // acquire the source, we can safely cast it to GUIOutput
+ GUIOutput source = (GUIOutput) event.getSource();
+
+ source.setState(GUIGeneState.NEUTRAL);
+ ((GUIConnection) ((Gene) source.getOutput().getSource()).getGUIObject()).setStateRecursively(GUIGeneState.NEUTRAL);
+ }
+ };
+
+ /**
+ * If the output is locked, unlock it and all of its associated genes recursively.
+ * If it is unlocked, lock it and its active genes.
+ */
+ private static EventHandler<MouseEvent> mouseClickHandler = new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ // acquire the source, we can safely cast it to GUIOutput
+ GUIOutput source = (GUIOutput) event.getSource();
+
+ boolean lock = !source.isLocked();
+ source.setLock(lock);
+ ((GUIConnection) ((Gene) source.getOutput().getSource()).getGUIObject()).setLockRecursively(lock);
+ }
+ };
+
+ /**
+ * Adds all handlers to the specified output.
+ *
+ * @param output the {@code GUIOutput} to which the handlers will be added.
+ */
+ public static void addHandlers(GUIOutput output) {
+ output.addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredHandler);
+ output.addEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedHandler);
+ output.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickHandler);
+ }
+
+}
diff --git a/src/jcgp/gui/handlers/Target.java b/src/jcgp/gui/handlers/Target.java
new file mode 100644
index 0000000..b050663
--- /dev/null
+++ b/src/jcgp/gui/handlers/Target.java
@@ -0,0 +1,70 @@
+package jcgp.gui.handlers;
+
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Line;
+import jcgp.gui.population.GUIConnection;
+import jcgp.gui.population.GUIMutable;
+
+/**
+ * @author Eduardo Pedroni
+ *
+ */
+public final class Target {
+
+ /**
+ * Private constructor to prevent instantiation.
+ */
+ private Target() {}
+
+ private static GUIConnection targetConnection;
+ private static GUIMutable sourceMutable;
+ private static int connectionIndex;
+ private static Line connectionLine;
+ private static Circle sourceSocket;
+ private static boolean prospecting = false;
+
+ public static void start(Circle newSocket) {
+ // store new socket
+ sourceSocket = newSocket;
+ // derive the rest of the information from it
+ connectionIndex = Integer.valueOf(newSocket.getId());
+ sourceMutable = (GUIMutable) newSocket.getParent();
+ connectionLine = sourceMutable.getLines()[connectionIndex];
+ }
+
+ public static GUIMutable getSourceMutable() {
+ return sourceMutable;
+ }
+
+ public static int getConnectionIndex() {
+ return connectionIndex;
+ }
+
+ public static Line getConnectionLine() {
+ return connectionLine;
+ }
+
+ public static Circle getSourceSocket() {
+ return sourceSocket;
+ }
+
+ public static GUIConnection getTarget() {
+ return targetConnection;
+ }
+
+ public static GUIConnection getCurrentConnection() {
+ return sourceMutable.getConnections()[connectionIndex];
+ }
+
+ public static void setProspecting(boolean value) {
+ prospecting = value;
+ }
+
+ public static boolean isProspecting() {
+ return prospecting;
+ }
+
+ public static void setTarget(GUIConnection newTarget) {
+ targetConnection = newTarget;
+ }
+}
diff --git a/src/jcgp/gui/population/ChromosomePane.java b/src/jcgp/gui/population/ChromosomePane.java
new file mode 100644
index 0000000..a87a054
--- /dev/null
+++ b/src/jcgp/gui/population/ChromosomePane.java
@@ -0,0 +1,140 @@
+package jcgp.gui.population;
+
+import java.util.ArrayList;
+
+import javafx.scene.control.ScrollPane;
+import javafx.scene.layout.Pane;
+import javafx.scene.shape.Line;
+import jcgp.backend.population.Chromosome;
+import jcgp.backend.population.Node;
+import jcgp.gui.GUI;
+
+/**
+ * This extension of {@code ScrollPane} contains a series of
+ * nodes, inputs and outputs spread across a grid. It also contains
+ * all of the connection lines laid over the nodes, inputs and outputs.
+ *
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public class ChromosomePane extends ScrollPane {
+
+ private GUIInput[] guiInputs;
+ private GUINode[][] guiNodes;
+ private GUIOutput[] guiOutputs;
+
+ private Pane content;
+
+ private boolean target = false;
+
+ public ChromosomePane(Chromosome chromosome) {
+ super();
+
+ ArrayList<Line> connectionLines = new ArrayList<Line>();
+
+ int rows = GUI.resources.rows();
+ int columns = GUI.resources.columns();
+
+ content = new Pane();
+ content.setId("content pane for genes");
+
+ /*
+ * inputs
+ */
+ guiInputs = new GUIInput[GUI.resources.inputs()];
+ for (int i = 0; i < guiInputs.length; i++) {
+ guiInputs[i] = new GUIInput(chromosome.getInput(i));
+ }
+ // add inputs to content pane
+ content.getChildren().addAll(guiInputs);
+
+ /*
+ * nodes
+ */
+ guiNodes = new GUINode[rows][columns];
+ for (int c = 0; c < columns; c++) {
+ for (int r = 0; r < rows; r++) {
+ // make the connection lines
+ Line lines[] = new Line[GUI.resources.arity()];
+ for (int l = 0; l < lines.length; l++) {
+ lines[l] = new Line();
+ lines[l].setMouseTransparent(true);
+ lines[l].setVisible(false);
+ connectionLines.add(lines[l]);
+ }
+ // make the GUI element
+ guiNodes[r][c] = new GUINode(chromosome.getNode(r, c), lines);
+ // add node to content pane
+ content.getChildren().add(guiNodes[r][c]);
+ }
+ }
+
+ /*
+ * outputs
+ */
+ guiOutputs = new GUIOutput[GUI.resources.outputs()];
+ for (int i = 0; i < guiOutputs.length; i++) {
+ // make the connection line
+ Line line = new Line();
+ line.setVisible(false);
+ line.setMouseTransparent(true);
+ connectionLines.add(line);
+ // make the GUI element
+ guiOutputs[i] = new GUIOutput(chromosome.getOutput(i), line);
+ }
+ // add outputs to content pane
+ content.getChildren().addAll(guiOutputs);
+
+ // add lines to the pane on top of genes
+ content.getChildren().addAll(connectionLines);
+
+ setPrefWidth(620);
+ setContent(content);
+ }
+
+ protected boolean isTarget() {
+ return target;
+ }
+
+ protected void setTarget(boolean newValue) {
+ target = newValue;
+ }
+
+ public void updateGenes(Chromosome chr) {
+ for (int r = 0; r < GUI.resources.rows(); r++) {
+ for (int c = 0; c < GUI.resources.columns(); c++) {
+ guiNodes[r][c].setNode(chr.getNode(r, c));
+ }
+ }
+ for (int i = 0; i < guiOutputs.length; i++) {
+ guiOutputs[i].setOutput(chr.getOutput(i));
+ }
+ }
+
+ public static boolean isAllowed(GUIMutable source, GUIConnection target) {
+ if (source instanceof GUINode) {
+ // if the source is a node, all inputs and some nodes are valid
+ if (target instanceof GUIInput) {
+ return true;
+ } else if (target instanceof GUINode) {
+ // target and source are nodes, let's look at levels back
+ Node t = ((GUINode) target).getNode(), s = ((GUINode) source).getNode();
+ if (s.getColumn() - t.getColumn() > 0 && s.getColumn() - t.getColumn() <= GUI.resources.levelsBack()) {
+ return true;
+ }
+ }
+ return false;
+ } else if (source instanceof GUIOutput) {
+ // if the source is an output, any node or input is valid
+ if (target instanceof GUINode || target instanceof GUIInput) {
+ return true;
+ } else {
+ // this should never happen...
+ return false;
+ }
+ }
+ // if the source was neither node nor output, something bad is happening
+ throw new ClassCastException("Source was neither GUINode nor GUIOutput.");
+ }
+}
diff --git a/src/jcgp/gui/population/FunctionSelector.java b/src/jcgp/gui/population/FunctionSelector.java
new file mode 100644
index 0000000..14614e5
--- /dev/null
+++ b/src/jcgp/gui/population/FunctionSelector.java
@@ -0,0 +1,80 @@
+package jcgp.gui.population;
+
+import javafx.event.EventHandler;
+import javafx.scene.control.Label;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.VBox;
+import jcgp.backend.function.FunctionSet;
+import jcgp.gui.constants.Constants;
+
+/**
+ * A menu class, exposes all of the allowed functions
+ * when called by a node, so that the node function can be changed.
+ *
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public class FunctionSelector extends VBox {
+
+ private GUINode target;
+
+ public FunctionSelector(FunctionSet functionSet) {
+ setFillWidth(true);
+ setVisible(false);
+ setStyle("-fx-border-color: #A0A0A0; -fx-border-width: 1 1 0 1");
+
+ remakeFunctions(functionSet);
+
+ addEventFilter(MouseEvent.MOUSE_EXITED, new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ dismiss();
+ }
+ });
+ }
+
+ public void remakeFunctions(final FunctionSet fs) {
+ getChildren().clear();
+
+ for (int i = 0; i < fs.getAllowedFunctionCount(); i++) {
+ final int index = i;
+ Label l = new Label(fs.getAllowedFunction(i).toString());
+ l.setMaxWidth(Double.MAX_VALUE);
+ l.setStyle("-fx-background-color: #FFFFFF; -fx-border-color: #A0A0A0; -fx-border-width: 0 0 1 0; -fx-padding: 2");
+
+ l.addEventFilter(MouseEvent.MOUSE_ENTERED, new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ ((Label) event.getSource()).setStyle("-fx-background-color: " + Constants.SOFT_HIGHLIGHT_PAINT + "; -fx-border-color: #B0B0B0; -fx-border-width: 0 0 1 0; -fx-padding: 2");
+ }
+ });
+ l.addEventFilter(MouseEvent.MOUSE_EXITED, new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ ((Label) event.getSource()).setStyle("-fx-background-color: #FFFFFF; -fx-border-color: #A0A0A0; -fx-border-width: 0 0 1 0; -fx-padding: 2");
+ }
+ });
+ l.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
+ @Override
+ public void handle(MouseEvent event) {
+ //target.setFunction(fs.getAllowedFunction(index));
+ dismiss();
+ }
+ });
+
+ getChildren().add(l);
+ }
+ }
+
+ public void relocateAndShow(MouseEvent event, GUINode node) {
+ relocate(event.getSceneX() - 5, event.getSceneY() - 5);
+ target = node;
+ setVisible(true);
+ }
+
+ private void dismiss() {
+ setVisible(false);
+ }
+
+}
diff --git a/src/jcgp/gui/population/GUIConnection.java b/src/jcgp/gui/population/GUIConnection.java
new file mode 100644
index 0000000..dc7fcc8
--- /dev/null
+++ b/src/jcgp/gui/population/GUIConnection.java
@@ -0,0 +1,32 @@
+package jcgp.gui.population;
+
+import jcgp.gui.population.GUIGene.GUIGeneState;
+
+/**
+ * A loose equivalent to {@link jcgp.backend.population.Connection}.
+ * <br>
+ * This defines behaviour that all GUI representations of connections
+ * should be capable of.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public interface GUIConnection {
+
+ /**
+ * Set the connection's state, but also recursively propagate that state
+ * all the way back to the inputs.
+ *
+ * @param state the state to set.
+ */
+ public void setStateRecursively(GUIGeneState state);
+
+ /**
+ * Add or remove a lock, but also recursively propagate that change
+ * all the way back to the inputs.
+ *
+ * @param value true to lock, false to unlock.
+ */
+ public void setLockRecursively(boolean value);
+
+}
diff --git a/src/jcgp/gui/population/GUIGene.java b/src/jcgp/gui/population/GUIGene.java
new file mode 100644
index 0000000..5e6107f
--- /dev/null
+++ b/src/jcgp/gui/population/GUIGene.java
@@ -0,0 +1,187 @@
+package jcgp.gui.population;
+
+import javafx.geometry.VPos;
+import javafx.scene.Group;
+import javafx.scene.paint.Paint;
+import javafx.scene.shape.Circle;
+import javafx.scene.text.Font;
+import javafx.scene.text.Text;
+import javafx.scene.text.TextAlignment;
+import jcgp.gui.constants.Constants;
+
+/**
+ * Defines the general behaviour of the visual representation of each chromosome gene.
+ * <br><br>
+ * In practice, this is subclass of {@code javafx.scene.Group} containing a {@code Circle}
+ * object and a {@code Text} object. Subclasses may add further elements to the group, for
+ * instance to display connection input and output sockets.
+ * <br><br>
+ * Genes also contain a locked property. When locked, some gene states behave slightly
+ * differently. This is used so genes remain highlighted even in the neutral state. The
+ * gene lock is in fact recursive; a gene can be locked multiple times and only unlocking
+ * it as many times will actually revert it back to its unlocked state. This allows multiple
+ * pathways to lock the same gene independently without affecting each other; the gene remains
+ * locked until no pathways are locking it.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public abstract class GUIGene extends Group {
+
+ /**
+ * This {@code enum} type defines a finite list of all states
+ * a gene can take. Each state represents a particular steady
+ * situation, and has its own GUI appearance associated with it:
+ * a combination of connection line visibility, gene background colour
+ * and other visual characteristics.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+ public enum GUIGeneState {
+ /**
+ * No user interaction at all.
+ */
+ NEUTRAL,
+ /**
+ * User is simply hovering over the node.
+ */
+ HOVER,
+ /**
+ * User is hovering over a node connected to this one.
+ */
+ EXTENDED_HOVER,
+ /**
+ * User is hovering over an output connected to this gene.
+ */
+ ACTIVE_HOVER,
+
+ GOOD_TARGET,
+
+ NEUTRAL_TARGET,
+
+ BAD_TARGET
+ }
+
+ private GUIGeneState currentState = GUIGeneState.NEUTRAL;
+
+ private Text text;
+ private Circle mainCircle;
+
+ /**
+ * Recursive lock; lock == 0 means unlocked, lock > 0 means locked.
+ * Accessing using {@code setLock(...)}.
+ */
+ private int lock = 0;
+
+ /**
+ * Initialises the {@code Text} and {@code Circle} objects so that all genes are standardised.
+ */
+ protected GUIGene() {
+ text = new Text();
+ text.setFont(Font.font("Arial", 12));
+ text.setTextOrigin(VPos.CENTER);
+ text.setTextAlignment(TextAlignment.CENTER);
+ text.setWrappingWidth(Constants.NODE_RADIUS * 2);
+ text.setX(-Constants.NODE_RADIUS);
+
+ mainCircle = new Circle(Constants.NODE_RADIUS, Constants.NEUTRAL_PAINT);
+ mainCircle.setStroke(Paint.valueOf("black"));
+
+ getChildren().addAll(mainCircle, text);
+ }
+
+ /**
+ * Sets the gene's text field.
+ *
+ * @param newText the text string to be displayed.
+ */
+ public void setText(String newText) {
+ text.setText(newText);
+ }
+
+ /**
+ * @return the gene's current state.
+ */
+ public GUIGeneState getState() {
+ return currentState;
+ }
+
+ /**
+ * Gene states are standardised: all gene subclasses behave the same way in each state.
+ * <br>
+ * This design choice was made for the sake of consistency. Rather than controlling the
+ * appearance of the genes with logic in the state transition method AND the mouse handlers,
+ * the states are now consistent across all types of gene. The mouse handlers implement
+ * whatever logic is necessary to determine the gene's new state given a certain user input,
+ * but the states themselves are the same for all genes.
+ * <br>
+ * The transition logic for each type of gene is defined in its respective handler class:
+ * {@code InputHandlers}, {@code NodeHandlers} and {@code OutputHandlers}.
+ *
+ * @param newState the gene's new state.
+ */
+ public final void setState(GUIGeneState newState) {
+ switch (newState) {
+ case NEUTRAL:
+ mainCircle.setFill(isLocked() ? Constants.HARD_HIGHLIGHT_PAINT : Constants.NEUTRAL_PAINT);
+ setLinesVisible(isLocked());
+ break;
+ case HOVER:
+ mainCircle.setFill(Constants.MEDIUM_HIGHLIGHT_PAINT);
+ setLinesVisible(true);
+ break;
+ case EXTENDED_HOVER:
+ mainCircle.setFill(Constants.SOFT_HIGHLIGHT_PAINT);
+ setLinesVisible(isLocked());
+ break;
+ case ACTIVE_HOVER:
+ mainCircle.setFill(Constants.SOFT_HIGHLIGHT_PAINT);
+ setLinesVisible(true);
+ break;
+ case GOOD_TARGET:
+ mainCircle.setFill(Constants.GOOD_SELECTION_PAINT);
+ break;
+ case NEUTRAL_TARGET:
+ mainCircle.setFill(Constants.NEUTRAL_SELECTION_PAINT);
+ break;
+ case BAD_TARGET:
+ mainCircle.setFill(Constants.BAD_SELECTION_PAINT);
+ break;
+ }
+ currentState = newState;
+ }
+
+ /**
+ * For the sake of practicality, all {@code GUIGene} instances must implement this
+ * method. It sets the visibility of all of the gene's lines, if it has any.
+ *
+ * @param value the visibility value.
+ */
+ protected abstract void setLinesVisible(boolean value);
+
+ /**
+ * @return true if the gene is locked, false otherwise.
+ */
+ public boolean isLocked() {
+ return lock > 0;
+ }
+
+ /**
+ * Locks or unlocks the gene once. Locked genes
+ * behave slightly differently in some states.
+ * <br>
+ * Unlocking an already unlocked gene does nothing.
+ *
+ * @param value true to lock, false to unlock;
+ */
+ public void setLock(boolean value) {
+ if (value) {
+ lock++;
+ } else if (lock > 0) {
+ lock--;
+ } else {
+ lock = 0;
+ }
+ }
+}
diff --git a/src/jcgp/gui/population/GUIInput.java b/src/jcgp/gui/population/GUIInput.java
new file mode 100644
index 0000000..3db7416
--- /dev/null
+++ b/src/jcgp/gui/population/GUIInput.java
@@ -0,0 +1,70 @@
+package jcgp.gui.population;
+
+import javafx.scene.paint.Paint;
+import javafx.scene.shape.Circle;
+import jcgp.backend.population.Input;
+import jcgp.gui.constants.Constants;
+import jcgp.gui.constants.Position;
+import jcgp.gui.handlers.InputHandlers;
+
+/**
+ * The GUI counterpart of {@link jcgp.backend.population.Input}. This is a
+ * subclass of {@code GUIGene} which represents a chromosome input.
+ *
+ * @author Eduardo Pedroni
+ */
+public class GUIInput extends GUIGene implements GUIConnection {
+
+ private Input input;
+
+ /**
+ * Instantiate {@code GUIInput} given an {@code Input}.
+ *
+ * @param input the associated backend input.
+ */
+ public GUIInput(final Input input) {
+ super();
+ // store the input, associate itself with it
+ this.input = input;
+ input.setGUIObject(this);
+
+ // inputs only have a single output socket
+ Circle outputSocket = new Circle(Constants.NODE_RADIUS, 0, Constants.SOCKET_RADIUS, Paint.valueOf("white"));
+ outputSocket.setStroke(Paint.valueOf("black"));
+ outputSocket.setId(String.valueOf(0));
+ getChildren().add(outputSocket);
+
+ // relocate to the right position, add mouse handlers
+ Position.place(this);
+ InputHandlers.addHandlers(this);
+ }
+
+ /**
+ * @return the {@code Input} instance associated with this object.
+ */
+ public Input getInput() {
+ return input;
+ }
+
+ /**
+ * Associates this instance with a new input.
+ *
+ * @param input the new input.
+ */
+ void setInput(Input input) {
+ this.input = input;
+ }
+
+ @Override
+ public void setStateRecursively(GUIGeneState state) {
+ setState(state);
+ }
+
+ @Override
+ protected void setLinesVisible(boolean value) {}
+
+ @Override
+ public void setLockRecursively(boolean value) {
+ setLock(value);
+ }
+}
diff --git a/src/jcgp/gui/population/GUIMutable.java b/src/jcgp/gui/population/GUIMutable.java
new file mode 100644
index 0000000..fa996e2
--- /dev/null
+++ b/src/jcgp/gui/population/GUIMutable.java
@@ -0,0 +1,20 @@
+package jcgp.gui.population;
+
+import javafx.scene.shape.Line;
+
+/**
+ * A loose equivalent to {@link jcgp.backend.population.Mutable}.
+ * <br>
+ * This defines behaviour that all GUI representations of mutables
+ * should be capable of.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public interface GUIMutable {
+
+ public Line[] getLines();
+
+ public GUIConnection[] getConnections();
+
+}
diff --git a/src/jcgp/gui/population/GUINode.java b/src/jcgp/gui/population/GUINode.java
new file mode 100644
index 0000000..1a32426
--- /dev/null
+++ b/src/jcgp/gui/population/GUINode.java
@@ -0,0 +1,134 @@
+package jcgp.gui.population;
+
+import javafx.scene.paint.Paint;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Line;
+import jcgp.backend.population.Gene;
+import jcgp.backend.population.Node;
+import jcgp.gui.GUI;
+import jcgp.gui.constants.Constants;
+import jcgp.gui.constants.Position;
+import jcgp.gui.handlers.NodeHandlers;
+
+/**
+ * The GUI counterpart of {@link jcgp.backend.population.Node}. This is a
+ * subclass of {@code GUIGene} which represents a chromosome node.
+ *
+ * @author Eduardo Pedroni
+ */
+public class GUINode extends GUIGene implements GUIMutable, GUIConnection {
+
+ private Node node;
+ private Line[] lines;
+ private Circle[] sockets;
+
+ /**
+ * Instantiate {@code GUINode} given a {@code Node} and the lines needed
+ * to show its connections.
+ *
+ * @param node the associated backend node.
+ * @param lines the lines used to display connections.
+ */
+ public GUINode(Node node, Line[] lines) {
+ super();
+ // store references, associate with node
+ this.node = node;
+ this.lines = lines;
+ node.setGUIObject(this);
+
+ // create the output socket
+ Circle output = new Circle(Constants.NODE_RADIUS, 0, Constants.SOCKET_RADIUS, Constants.SOCKET_PAINT);
+ output.setStroke(Paint.valueOf("black"));
+
+ // create input sockets
+ sockets = new Circle[GUI.resources.arity()];
+ for (int l = 0; l < sockets.length; l++) {
+ sockets[l] = new Circle(Constants.SOCKET_RADIUS, Constants.SOCKET_PAINT);
+ sockets[l].setStroke(Paint.valueOf("black"));
+ sockets[l].setId(String.valueOf(l));
+ // relocate them
+ Position.placeSocket(l, sockets[l]);
+ Position.connect(lines[l], (GUIGene) ((Gene) node.getConnection(l)).getGUIObject());
+ }
+
+ // add elements
+ getChildren().addAll(sockets);
+ getChildren().add(output);
+
+ // relocate node, add handlers
+ Position.place(this);
+ NodeHandlers.addHandlers(this);
+ }
+
+ /**
+ * @return the {@code Node} instance associated with this object.
+ */
+ public Node getNode() {
+ return node;
+ }
+
+ /**
+ * Associates this instance with a new node.
+ *
+ * @param node the new node.
+ */
+ void setNode(Node node) {
+ this.node = node;
+ }
+
+ @Override
+ public Line[] getLines() {
+ return lines;
+ }
+
+ /**
+ * Returns one of this object's connection sockets. They are
+ * indexed in the same order as lines and the connections
+ * they represent.
+ *
+ * @param index the socket to return.
+ * @return the indexed socket object.
+ */
+ public Circle getSocket(int index) {
+ return sockets[index];
+ }
+
+ /**
+ * @return the entire {@code Socket} array.
+ */
+ public Circle[] getSockets() {
+ return sockets;
+ }
+
+ @Override
+ public void setStateRecursively(GUIGeneState state) {
+ setState(state);
+ for (int i = 0; i < GUI.resources.arity(); i++) {
+ ((GUIConnection) ((Gene) node.getConnection(i)).getGUIObject()).setStateRecursively(state);
+ }
+ }
+
+ @Override
+ protected void setLinesVisible(boolean value) {
+ for (int i = 0; i < lines.length; i++) {
+ lines[i].setVisible(value);
+ }
+ }
+
+ @Override
+ public void setLockRecursively(boolean value) {
+ setLock(value);
+ for (int i = 0; i < GUI.resources.arity(); i++) {
+ ((GUIConnection) ((Gene) node.getConnection(i)).getGUIObject()).setLockRecursively(value);
+ }
+ }
+
+ @Override
+ public GUIConnection[] getConnections() {
+ GUIConnection[] connections = new GUIConnection[GUI.resources.arity()];
+ for (int c = 0; c < connections.length; c++) {
+ connections[c] = (GUIConnection) ((Gene) node.getConnection(c)).getGUIObject();
+ }
+ return connections;
+ }
+}
diff --git a/src/jcgp/gui/population/GUIOutput.java b/src/jcgp/gui/population/GUIOutput.java
new file mode 100644
index 0000000..f023d00
--- /dev/null
+++ b/src/jcgp/gui/population/GUIOutput.java
@@ -0,0 +1,79 @@
+package jcgp.gui.population;
+
+import javafx.scene.paint.Paint;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Line;
+import jcgp.backend.population.Gene;
+import jcgp.backend.population.Output;
+import jcgp.gui.constants.Constants;
+import jcgp.gui.constants.Position;
+import jcgp.gui.handlers.OutputHandlers;
+
+/**
+ * The GUI counterpart of {@link jcgp.backend.population.Output}. This is a
+ * subclass of {@code GUIGene} which represents a chromosome output.
+ *
+ * @author Eduardo Pedroni
+ */
+public class GUIOutput extends GUIGene implements GUIMutable {
+
+ private Output output;
+ private Line line;
+
+ /**
+ * Instantiate {@code GUIOutput} given an {@code Output} and the line needed
+ * to show its connection.
+ *
+ * @param output the associated backend output.
+ * @param line the line used to display connection.
+ */
+ public GUIOutput(final Output output, Line line) {
+ super();
+ // store references, associate with backend object
+ this.output = output;
+ this.line = line;
+ output.setGUIObject(this);
+
+ // create input socket
+ Circle socket = new Circle(-Constants.NODE_RADIUS, 0, Constants.SOCKET_RADIUS, Constants.SOCKET_PAINT);
+ socket.setStroke(Paint.valueOf("black"));
+ socket.setId(String.valueOf(0));
+ Position.connect(line, (GUIGene) ((Gene) output.getSource()).getGUIObject());
+ getChildren().add(socket);
+
+ // relocate output, add handlers
+ Position.place(this);
+ OutputHandlers.addHandlers(this);
+ }
+
+ /**
+ * @return the {@code Output} instance associated with this object.
+ */
+ public Output getOutput() {
+ return output;
+ }
+
+ /**
+ * Associates this instance with a new output.
+ *
+ * @param output the new output.
+ */
+ void setOutput(Output output) {
+ this.output = output;
+ }
+
+ @Override
+ public Line[] getLines() {
+ return new Line[] {line};
+ }
+
+ @Override
+ protected void setLinesVisible(boolean value) {
+ line.setVisible(value);
+ }
+
+ @Override
+ public GUIConnection[] getConnections() {
+ return new GUIConnection[] {(GUIConnection) output.getGUIObject()};
+ }
+}
diff --git a/src/jcgp/gui/population/PopulationPane.java b/src/jcgp/gui/population/PopulationPane.java
new file mode 100644
index 0000000..51b5ba4
--- /dev/null
+++ b/src/jcgp/gui/population/PopulationPane.java
@@ -0,0 +1,75 @@
+package jcgp.gui.population;
+
+import javafx.scene.control.Tab;
+import javafx.scene.control.TabPane;
+import jcgp.JCGP;
+import jcgp.backend.modules.problem.TestCaseProblem;
+import jcgp.backend.modules.problem.TestCaseProblem.TestCase;
+import jcgp.gui.GUI;
+
+public class PopulationPane extends TabPane {
+
+ private GUI gui;
+ private TestCase<Object> currentTestCase;
+ private boolean evaluating = false;
+
+ public PopulationPane(GUI gui) {
+ super();
+ this.gui = gui;
+ setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
+ remakeTabs();
+ }
+
+ public void remakeTabs() {
+ getTabs().clear();
+ JCGP jcgp = gui.getExperiment();
+
+ Tab tab;
+ ChromosomePane cp;
+ for (int i = 0; i < jcgp.getResources().populationSize(); i++) {
+ cp = new ChromosomePane(jcgp.getPopulation().get(i));
+ tab = new Tab("Chr " + i);
+ tab.setContent(cp);
+ getTabs().add(tab);
+ }
+ }
+
+ public void updateGenes() {
+ if (evaluating) {
+ evaluateTestCase(currentTestCase);
+ }
+ for (int i = 0; i < getTabs().size(); i++) {
+ ((ChromosomePane) getTabs().get(i).getContent()).updateGenes(gui.getExperiment().getPopulation().get(i));
+ }
+ }
+
+ public void evaluateTestCase(TestCase<Object> testCase) {
+ if (gui.getExperiment().getProblem() instanceof TestCaseProblem && testCase != null) {
+ currentTestCase = testCase;
+ if (testCase.getInputs().length == gui.getExperiment().getResources().inputs()) {
+ evaluating = true;
+ for (int i = 0; i < getTabs().size(); i++) {
+ //((ChromosomePane) getTabs().get(i).getContent()).setInputs(testCase.getInputs());
+ }
+ } else {
+ throw new IllegalArgumentException("Test case has " + testCase.getInputs().length
+ + " inputs and chromosome has " + gui.getExperiment().getResources().inputs());
+ }
+ }
+ }
+
+ public void hideValues() {
+ evaluating = false;
+ for (int i = 0; i < getTabs().size(); i++) {
+ //((ChromosomePane) getTabs().get(i).getContent()).updateValues();
+ }
+ }
+
+ public boolean isEvaluating() {
+ return evaluating;
+ }
+
+ public void setEvaluating(boolean value) {
+ evaluating = value;
+ }
+}
diff --git a/src/jcgp/gui/settings/SettingsPane.java b/src/jcgp/gui/settings/SettingsPane.java
new file mode 100644
index 0000000..bad42cd
--- /dev/null
+++ b/src/jcgp/gui/settings/SettingsPane.java
@@ -0,0 +1,595 @@
+package jcgp.gui.settings;
+
+import java.io.File;
+import java.util.ArrayList;
+
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Font;
+import javafx.scene.text.Text;
+import javafx.stage.FileChooser;
+import javafx.stage.FileChooser.ExtensionFilter;
+import jcgp.JCGP;
+import jcgp.backend.function.FunctionSet;
+import jcgp.backend.modules.es.EvolutionaryStrategy;
+import jcgp.backend.modules.mutator.Mutator;
+import jcgp.backend.modules.problem.Problem;
+import jcgp.backend.modules.problem.TestCaseProblem;
+import jcgp.backend.parameters.Parameter;
+import jcgp.gui.GUI;
+import jcgp.gui.constants.Constants;
+import jcgp.gui.settings.parameters.GUIParameter;
+import jcgp.gui.settings.testcase.TestCaseTable;
+
+/**
+ * This is a fairly hefty class which encapsulates the entire right-hand
+ * control pane. It contains base parameters, module selectors and their
+ * associated parameters, flow controls and file loading/saving buttons.
+ * <br><br>
+ * A single instance of this class is used in {@code GUI}.
+ *
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public class SettingsPane extends AnchorPane {
+
+ /*
+ * The primary containers, these make up each section of the settings pane.
+ */
+ private VBox mainContainer;
+ private VBox baseParameterPane, eaPane, mutatorPane, problemPane;
+ private VBox nodeFunctions;
+
+ // all buttons
+ private Button runPause = new Button("Run"), step = new Button("Step"), reset = new Button("Reset");
+ private Button loadParameters = new Button("Load parameters"), loadChromosome = new Button("Load chromosome"), saveChromosome = new Button("Save chromosome");
+
+ // this is a list of parameters used for parameter validity checks
+ private ArrayList<GUIParameter<?>> parameters = new ArrayList<GUIParameter<?>>();
+
+ // the test case table stage
+ private TestCaseTable testCaseTable;
+
+ // a reference to the parent GUI
+ private GUI gui;
+
+ private int currentArity;
+
+ /**
+ * Create a new instance of {@code SettingsPane} associated
+ * with the specified {@code GUI} object.
+ *
+ * @param gui a reference to this object's parent.
+ */
+ public SettingsPane(GUI gui) {
+ super();
+ this.gui = gui;
+
+ // acquire a reference to jcgp, for convenience
+ final JCGP jcgp = gui.getExperiment();
+
+ // make the overarching container
+ mainContainer = new VBox(8);
+ mainContainer.setPadding(new Insets(5, Constants.RESIZE_MARGIN, 0, 2));
+
+ setMinWidth(Constants.SETTINGS_MIN_WIDTH);
+ setPrefWidth(Constants.SETTINGS_MIN_WIDTH);
+
+ // initialise all sub-divisions
+ initialiseBaseParameters(jcgp);
+
+ initialiseEAParameters(jcgp);
+
+ initialiseMutatorParameters(jcgp);
+
+ initialiseProblemTypeParameters(jcgp, gui);
+
+ createControls(gui);
+
+ // prepare the scroll pane
+ ScrollPane scroll = new ScrollPane();
+ scroll.setFitToWidth(true);
+ scroll.setContent(mainContainer);
+ scroll.setStyle("-fx-background-color: #FFFFFF");
+
+ // anchor the scroll pane to itself, bearing in mind the resize margin
+ AnchorPane.setTopAnchor(scroll, 0.0);
+ AnchorPane.setBottomAnchor(scroll, 0.0);
+ AnchorPane.setRightAnchor(scroll, 0.0);
+ AnchorPane.setLeftAnchor(scroll, Constants.RESIZE_MARGIN);
+
+ // add the scroll pane, all done!
+ getChildren().add(scroll);
+ }
+
+ /**
+ * Creates the base parameters pane
+ *
+ * @param jcgp
+ */
+ private void initialiseBaseParameters(JCGP jcgp) {
+ baseParameterPane = new VBox(2);
+
+ Text header = new Text("Base Parameters");
+ header.setFont(Font.font("Arial", 14));
+ header.setUnderline(true);
+
+ baseParameterPane.getChildren().add(header);
+
+ parameters.add(GUIParameter.create(jcgp.getResources().getRowsParameter(), this));
+ parameters.add(GUIParameter.create(jcgp.getResources().getColumnsParameter(), this));
+ parameters.add(GUIParameter.create(jcgp.getResources().getInputsParameter(), this));
+ parameters.add(GUIParameter.create(jcgp.getResources().getOutputsParameter(), this));
+ parameters.add(GUIParameter.create(jcgp.getResources().getLevelsBackParameter(), this));
+
+ GUIParameter<?> gp = GUIParameter.create(jcgp.getResources().getPopulationSizeParameter(), this);
+ gp.setPadding(new Insets(0, 0, 10, 0));
+ parameters.add(gp);
+
+ parameters.add(GUIParameter.create(jcgp.getResources().getCurrentGenerationParameter(), this));
+ parameters.add(GUIParameter.create(jcgp.getResources().getGenerationsParameter(), this));
+ parameters.add(GUIParameter.create(jcgp.getResources().getCurrentRunParameter(), this));
+
+ gp = GUIParameter.create(jcgp.getResources().getRunsParameter(), this);
+ gp.setPadding(new Insets(0, 0, 10, 0));
+ parameters.add(gp);
+
+ parameters.add(GUIParameter.create(jcgp.getResources().getSeedParameter(), this));
+ parameters.add(GUIParameter.create(jcgp.getResources().getReportIntervalParameter(), this));
+
+ baseParameterPane.getChildren().addAll(parameters);
+ mainContainer.getChildren().add(baseParameterPane);
+ }
+
+ private void initialiseEAParameters(final JCGP jcgp) {
+ eaPane = new VBox(2);
+
+ Text header = new Text("Evolutionary Strategy");
+ header.setFont(Font.font("Arial", 14));
+ header.setUnderline(true);
+
+ final ComboBox<EvolutionaryStrategy> esCBox = new ComboBox<EvolutionaryStrategy>();
+ esCBox.getItems().addAll(jcgp.getEvolutionaryStrategies());
+ esCBox.getSelectionModel().select(jcgp.getEvolutionaryStrategy());
+ esCBox.prefWidthProperty().bind(mainContainer.widthProperty());
+
+ final VBox eaParameters = new VBox(2);
+
+ refreshParameters(jcgp.getEvolutionaryStrategy().getLocalParameters(), eaParameters);
+
+ esCBox.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ jcgp.setEvolutionaryStrategy(esCBox.getSelectionModel().getSelectedIndex());
+ refreshParameters(esCBox.getSelectionModel().getSelectedItem().getLocalParameters(), eaParameters);
+ gui.flushConsole();
+ }
+ });
+
+ eaPane.getChildren().addAll(header, esCBox, eaParameters);
+ mainContainer.getChildren().add(eaPane);
+ }
+
+ private void initialiseMutatorParameters(final JCGP jcgp) {
+ mutatorPane = new VBox(2);
+
+ Text header = new Text("Mutator");
+ header.setFont(Font.font("Arial", 14));
+ header.setUnderline(true);
+
+ final ComboBox<Mutator> mutatorCBox = new ComboBox<Mutator>();
+ mutatorCBox.getItems().addAll(jcgp.getMutators());
+ mutatorCBox.getSelectionModel().select(jcgp.getMutator());
+ mutatorCBox.prefWidthProperty().bind(mainContainer.widthProperty());
+
+ final VBox mutatorParameters = new VBox(2);
+ refreshParameters(jcgp.getMutator().getLocalParameters(), mutatorParameters);
+
+ mutatorCBox.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ jcgp.setMutator(mutatorCBox.getSelectionModel().getSelectedIndex());
+ refreshParameters(mutatorCBox.getSelectionModel().getSelectedItem().getLocalParameters(), mutatorParameters);
+ gui.flushConsole();
+ }
+ });
+
+ mutatorPane.getChildren().addAll(header, mutatorCBox, mutatorParameters);
+ mainContainer.getChildren().add(mutatorPane);
+ }
+
+ private void initialiseProblemTypeParameters(final JCGP jcgp, final GUI gui) {
+ updateArity();
+
+ problemPane= new VBox(2);
+
+ Text header = new Text("Problem Type");
+ header.setFont(Font.font("Arial", 14));
+ header.setUnderline(true);
+
+ final ComboBox<Problem> problemCBox = new ComboBox<Problem>();
+ problemCBox.getItems().addAll(jcgp.getProblems());
+ problemCBox.getSelectionModel().select(jcgp.getProblem());
+ problemCBox.prefWidthProperty().bind(mainContainer.widthProperty());
+
+ final VBox problemParameters = new VBox(2);
+ problemParameters.setPadding(new Insets(0, 0, 4, 0));
+ refreshParameters(jcgp.getProblem().getLocalParameters(), problemParameters);
+
+ final HBox testCaseControlContainer = new HBox(2);
+
+ final Button showTestCaseButton = makeTestCaseButton();
+ final Button loadProblemDataButton = makeLoadTestCaseButton();
+ HBox.setHgrow(showTestCaseButton, Priority.ALWAYS);
+ showTestCaseButton.setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(loadProblemDataButton, Priority.ALWAYS);
+ loadProblemDataButton.setMaxWidth(Double.MAX_VALUE);
+
+ if (jcgp.getProblem() instanceof TestCaseProblem<?>) {
+ testCaseControlContainer.getChildren().addAll(showTestCaseButton, loadProblemDataButton);
+ remakeTestCaseTable();
+ } else {
+ testCaseControlContainer.getChildren().add(loadProblemDataButton);
+ }
+
+ nodeFunctions = new VBox(2);
+ nodeFunctions.setPadding(new Insets(0, 0, 4, 0));
+ refreshFunctions();
+
+ problemCBox.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ jcgp.setProblem(problemCBox.getSelectionModel().getSelectedIndex());
+ updateArity();
+ refreshParameters(jcgp.getProblem().getLocalParameters(), problemParameters);
+ if (testCaseTable != null) {
+ testCaseTable.close();
+ }
+ gui.setEvaluating(false);
+ refreshFunctions();
+ testCaseControlContainer.getChildren().clear();
+ if (jcgp.getProblem() instanceof TestCaseProblem) {
+ testCaseControlContainer.getChildren().addAll(showTestCaseButton, loadProblemDataButton);
+ remakeTestCaseTable();
+ } else {
+ testCaseControlContainer.getChildren().add(loadProblemDataButton);
+ }
+ gui.reset();
+ }
+ });
+
+ problemPane.getChildren().addAll(header, problemCBox, problemParameters, nodeFunctions, testCaseControlContainer);
+ mainContainer.getChildren().add(problemPane);
+
+ }
+
+ private Button makeLoadTestCaseButton() {
+ Button b = new Button("Load data");
+ b.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ FileChooser fc = new FileChooser();
+ fc.setTitle("Open problem file...");
+ fc.getExtensionFilters().add(new ExtensionFilter("CGP " + gui.getExperiment().getProblem() + " files", "*" + ((TestCaseProblem<?>) gui.getExperiment().getProblem()).getFileExtension()));
+ fc.getExtensionFilters().add(new ExtensionFilter("All files", "*.*"));
+ File chrFile = fc.showOpenDialog(gui.getStage());
+ if (chrFile != null) {
+ gui.getExperiment().loadProblemData(chrFile);
+ remakeTestCaseTable();
+ gui.reDraw();
+ }
+ }
+ });
+ return b;
+ }
+
+ private Button makeTestCaseButton() {
+ Button b = new Button("Show data");
+ b.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ testCaseTable.show();
+ }
+ });
+ return b;
+ }
+
+ private void createControls(final GUI gui) {
+ Text header = new Text("Experiment controls");
+ header.setFont(Font.font("Arial", 14));
+ header.setUnderline(true);
+
+ final VBox controls = new VBox(2);
+ controls.setFillWidth(true);
+
+ final HBox flowButtons = new HBox(2);
+ runPause.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ gui.runPause();
+ }
+ });
+
+ step.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ gui.step();
+ }
+ });
+
+ reset.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ gui.reset();
+ }
+ });
+
+ HBox.setHgrow(runPause, Priority.ALWAYS);
+ runPause.setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(step, Priority.ALWAYS);
+ step.setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(reset, Priority.ALWAYS);
+ reset.setMaxWidth(Double.MAX_VALUE);
+
+ flowButtons.getChildren().addAll(runPause, step, reset);
+ flowButtons.setPadding(new Insets(0, 0, 10, 0));
+
+ loadParameters.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ FileChooser fc = new FileChooser();
+ fc.setTitle("Open .par file...");
+ fc.getExtensionFilters().add(new ExtensionFilter("CGP parameter files", "*.par"));
+ fc.getExtensionFilters().add(new ExtensionFilter("All files", "*.*"));
+ File parFile = fc.showOpenDialog(gui.getStage());
+ if (parFile != null) {
+ gui.getExperiment().loadParameters(parFile);
+ gui.reDraw();
+ refreshFunctions();
+ }
+ gui.flushConsole();
+ }
+ });
+
+ loadChromosome.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ FileChooser fc = new FileChooser();
+ fc.setTitle("Load .chr file...");
+ fc.getExtensionFilters().add(new ExtensionFilter("CGP chromosome files", "*.chr"));
+ fc.getExtensionFilters().add(new ExtensionFilter("All files", "*.*"));
+ File chrFile = fc.showOpenDialog(gui.getStage());
+ if (chrFile != null) {
+ gui.getExperiment().loadChromosome(chrFile, gui.getChromosomeIndex());
+ gui.reDraw();
+ }
+ gui.flushConsole();
+ }
+ });
+ saveChromosome.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ FileChooser fc = new FileChooser();
+ fc.setTitle("Save .chr file...");
+ fc.getExtensionFilters().add(new ExtensionFilter("CGP chromosome files", "*.chr"));
+ fc.getExtensionFilters().add(new ExtensionFilter("All files", "*.*"));
+ File chrFile = fc.showSaveDialog(gui.getStage());
+ if (chrFile != null) {
+ gui.getExperiment().saveChromosome(chrFile, gui.getChromosomeIndex());
+ }
+ gui.flushConsole();
+ }
+ });
+
+ HBox.setHgrow(loadParameters, Priority.ALWAYS);
+ loadParameters.setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(loadChromosome, Priority.ALWAYS);
+ loadChromosome.setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(saveChromosome, Priority.ALWAYS);
+ saveChromosome.setMaxWidth(Double.MAX_VALUE);
+
+ controls.getChildren().addAll(header, flowButtons, loadParameters, loadChromosome, saveChromosome);
+
+ mainContainer.getChildren().add(controls);
+ }
+
+ /**
+ * Builds {@code GUIParameter}s and adds them to the provided {@code VBox}.
+ * The parameters built are taken from the specified list.
+ *
+ * @param newParameters the list of parameters to add.
+ * @param container the container to add the parameters to.
+ */
+ private void refreshParameters(ArrayList<Parameter<?>> newParameters, VBox container) {
+ // remove what is currently in the container from the parameter list
+ parameters.removeAll(container.getChildren());
+ // remove everything in the container
+ container.getChildren().clear();
+ // if there are parameters to add, add them all
+ if (newParameters != null) {
+ for (int i = 0; i < newParameters.size(); i++) {
+ // factory method returns the right subtype of GUIParameter
+ GUIParameter<?> guiParameter = GUIParameter.create(newParameters.get(i), this);
+ // make sure to add it to the parameter list as well
+ parameters.add(guiParameter);
+ container.getChildren().add(guiParameter);
+ }
+ }
+ // do a quick refresh just in case something is invalid
+ revalidateParameters();
+ }
+
+ /**
+ * This method handles a problem type change by updating the list of allowed
+ * node functions.
+ * <br><br>
+ * It does so by creating new checkboxes for each function in the function set.
+ */
+ private void refreshFunctions() {
+ // remove all current functions
+ nodeFunctions.getChildren().clear();
+ CheckBox checkBox;
+ // get a reference to the function set
+ final FunctionSet functionSet = gui.getExperiment().getResources().getFunctionSet();
+ for (int i = 0; i < functionSet.getTotalFunctionCount(); i++) {
+ // add a checkbox for each function
+ checkBox = new CheckBox(functionSet.getFunction(i).toString());
+ checkBox.setId(String.valueOf(i));
+ // make sure the selection matches the function set
+ checkBox.setSelected(functionSet.isEnabled(functionSet.getFunction(i)));
+ final int index = i;
+ // set listener so function set gets updated if the checkboxes change
+ checkBox.setOnAction(new EventHandler<ActionEvent>() {
+ @Override
+ public void handle(ActionEvent event) {
+ if (((CheckBox) event.getSource()).isSelected()) {
+ functionSet.enableFunction(index);
+ } else {
+ functionSet.disableFunction(index);
+ }
+ gui.updateFunctionSelector();
+ revalidateParameters();
+ }
+ });
+ // add the new checkbox
+ nodeFunctions.getChildren().add(checkBox);
+ }
+ // make sure function selector has all functions
+ gui.updateFunctionSelector();
+ }
+
+ /**
+ * @return true if the experiment is currently evolving something, false otherwise.
+ */
+ public boolean isExperimentRunning() {
+ return gui.isWorking();
+ }
+
+ /**
+ *
+ * @return true if the experiment needs to be reset, false if otherwise.
+ */
+ public boolean isResetRequired() {
+ for (GUIParameter<?> parameter : parameters) {
+ if (parameter.requiresReset()) {
+ return true;
+ }
+ }
+ if (arityChanged()) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return true if no parameters have their status set to ParameterStatus.INVALID.
+ */
+ public boolean areParametersValid() {
+ for (GUIParameter<?> parameter : parameters) {
+ if (!parameter.isValid()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Calls validate() on every parameter. This is called whenever a parameter changes,
+ * so that other parameters update their status in case they were dependent on the
+ * changed parameter.
+ * <br><br>
+ * This also disables the controls if a reset is necessary, preventing the experiment
+ * from running until it has happened.
+ */
+ public void revalidateParameters() {
+ boolean disableControls = false;
+ for (GUIParameter<?> parameter : parameters) {
+ parameter.validate();
+ if (parameter.requiresReset()) {
+ disableControls = true;
+ }
+ }
+ if (arityChanged()) {
+ disableControls = true;
+ }
+
+ runPause.setDisable(disableControls);
+ step.setDisable(disableControls);
+ }
+
+ /**
+ * Calls applyValue() on every parameter. This is called when a reset occurs, so that
+ * the new value will be used as a reference instead of the old reference value.
+ * <br><br>
+ * It also closes the test case table, just in case.
+ */
+ public void applyParameters() {
+ for (GUIParameter<?> parameter : parameters) {
+ parameter.applyValue();
+ }
+ updateArity();
+ if (testCaseTable != null) {
+ testCaseTable.close();
+ }
+ }
+
+ /**
+ * Updates all of the controls to their appropriate state based on the status of the
+ * experiment, in order to prevent inappropriate operations if the experiment is
+ * running or finished.
+ *
+ * @param running true if the experiment is running.
+ * @param finished true if the experiment is finished.
+ */
+ public void updateControls(boolean running, boolean finished) {
+ baseParameterPane.setDisable(running);
+ eaPane.setDisable(running);
+ mutatorPane.setDisable(running);
+ problemPane.setDisable(running);
+
+ runPause.setText(running ? "Pause" : "Run");
+ runPause.setDisable(finished);
+ step.setDisable(running || finished);
+ reset.setDisable(running);
+
+ loadParameters.setDisable(running);
+ loadChromosome.setDisable(running);
+ saveChromosome.setDisable(running);
+
+ testCaseTable.getTable().setDisable(running);
+ }
+
+ private void remakeTestCaseTable() {
+ boolean wasShowing = false;
+ if (testCaseTable != null) {
+ wasShowing = testCaseTable.isShowing();
+ testCaseTable.close();
+ }
+ testCaseTable = new TestCaseTable((TestCaseProblem<Object>) gui.getExperiment().getProblem(), gui);
+ if (wasShowing) {
+ testCaseTable.show();
+ }
+ }
+
+ public TestCaseTable getTestCaseTable() {
+ return testCaseTable;
+ }
+
+ private void updateArity() {
+ currentArity = gui.getExperiment().getProblem().getFunctionSet().getMaxArity();
+ }
+
+ private boolean arityChanged() {
+ return currentArity != gui.getExperiment().getProblem().getFunctionSet().getMaxArity();
+ }
+}
diff --git a/src/jcgp/gui/settings/parameters/GUIBooleanParameter.java b/src/jcgp/gui/settings/parameters/GUIBooleanParameter.java
new file mode 100644
index 0000000..a1f03fe
--- /dev/null
+++ b/src/jcgp/gui/settings/parameters/GUIBooleanParameter.java
@@ -0,0 +1,82 @@
+package jcgp.gui.settings.parameters;
+
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Control;
+import jcgp.backend.parameters.BooleanParameter;
+import jcgp.backend.parameters.ParameterStatus;
+import jcgp.gui.constants.Constants;
+import jcgp.gui.settings.SettingsPane;
+
+/**
+ * This extension of @code{GUIParameter} uses a @code{CheckBox} to display
+ * the value of a @code{BooleanParameter}. It cannot be constructed
+ * directly - instead, use @code{GUIParameter.create()}.
+ * <br><br>
+ * See {@link GUIParameter} for more information.
+ *
+ * @author Eduardo Pedroni
+ */
+public class GUIBooleanParameter extends GUIParameter<Boolean> {
+
+ private CheckBox checkBox;
+
+ /**
+ * This protected constructor is intended for use
+ * by the factory method only.
+ *
+ */
+ protected GUIBooleanParameter(BooleanParameter parameter, SettingsPane sp) {
+ super(parameter, sp);
+ }
+
+ @Override
+ protected Control makeControl() {
+ checkBox = new CheckBox();
+ checkBox.setSelected(parameter.get());
+
+ return checkBox;
+ }
+
+ @Override
+ protected void setControlListeners() {
+ /* pass the CheckBox value back to the parameter whenever it gets
+ * modified, provided the experiment isn't running */
+ checkBox.selectedProperty().addListener(new ChangeListener<Boolean>() {
+ @Override
+ public void changed(
+ ObservableValue<? extends Boolean> observable,
+ Boolean oldValue, Boolean newValue) {
+ if (!settingsPane.isExperimentRunning()) {
+ parameter.set(newValue);
+ settingsPane.revalidateParameters();
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void setValidityStyle() {
+ // update the Control's style and tooltip based on the status of the parameter
+ if (parameter.getStatus() == ParameterStatus.INVALID) {
+ checkBox.setStyle(Constants.BASE_CHECKBOX_STYLE + Constants.INVALID_PARAMETER_STYLE);
+ checkBox.setTooltip(tooltip);
+ tooltip.setText(parameter.getStatus().getDetails());
+ } else if (parameter.getStatus() == ParameterStatus.WARNING
+ || parameter.getStatus() == ParameterStatus.WARNING_RESET) {
+ checkBox.setStyle(Constants.BASE_CHECKBOX_STYLE + Constants.WARNING_PARAMETER_STYLE);
+ checkBox.setTooltip(tooltip);
+ tooltip.setText(parameter.getStatus().getDetails());
+ } else {
+ checkBox.setStyle(Constants.BASE_CHECKBOX_STYLE + Constants.VALID_PARAMETER_STYLE);
+ checkBox.setTooltip(null);
+ }
+ }
+
+ @Override
+ public void refreshValue() {
+ checkBox.setSelected(parameter.get());
+ }
+
+}
diff --git a/src/jcgp/gui/settings/parameters/GUIDoubleParameter.java b/src/jcgp/gui/settings/parameters/GUIDoubleParameter.java
new file mode 100644
index 0000000..feee34c
--- /dev/null
+++ b/src/jcgp/gui/settings/parameters/GUIDoubleParameter.java
@@ -0,0 +1,110 @@
+package jcgp.gui.settings.parameters;
+
+import java.text.DecimalFormat;
+
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.geometry.Pos;
+import javafx.scene.control.Control;
+import javafx.scene.control.TextField;
+import jcgp.backend.parameters.DoubleParameter;
+import jcgp.backend.parameters.ParameterStatus;
+import jcgp.gui.constants.Constants;
+import jcgp.gui.settings.SettingsPane;
+
+/**
+ * This extension of @code{GUIParameter} uses a @code{TextField} to display
+ * the value of a @code{DoubleParameter}. It cannot be constructed
+ * directly - instead, use @code{GUIParameter.create()}.
+ * <br><br>
+ * See {@link GUIParameter} for more information.
+ *
+ * @author Eduardo Pedroni
+ */
+public class GUIDoubleParameter extends GUIParameter<Number> {
+
+ private TextField textField;
+ private DecimalFormat decimalFormat;
+
+ /**
+ * This protected constructor is intended for use
+ * by the factory method only.
+ *
+ */
+ protected GUIDoubleParameter(DoubleParameter parameter, SettingsPane sp) {
+ super(parameter, sp);
+ }
+
+ @Override
+ protected Control makeControl() {
+ // we use a text field, and a formatting class to enforce decimals
+ decimalFormat = new DecimalFormat();
+ decimalFormat.setMaximumFractionDigits(10);
+ textField = new TextField(decimalFormat.format(parameter.get().doubleValue()));
+ textField.setStyle(Constants.VALID_PARAMETER_STYLE);
+ textField.setAlignment(Pos.CENTER_RIGHT);
+ textField.prefWidthProperty().bind(widthProperty().divide(2));
+ return textField;
+ }
+
+ @Override
+ protected void setControlListeners() {
+ /* pass the TextField value back to the parameter whenever it gets
+ * modified, provided it is not empty, the experiment isn't running
+ * and it matches the double-precision regex filter */
+ textField.textProperty().addListener(new ChangeListener<String>() {
+ @Override
+ public void changed(
+ ObservableValue<? extends String> observable,
+ String oldValue, String newValue) {
+ if (!settingsPane.isExperimentRunning()) {
+ if (newValue.matches("^[-+]?[0-9]*\\.?[0-9]+$")) {
+ if (!newValue.isEmpty()) {
+ double value = Double.parseDouble(newValue);
+ parameter.set(value);
+ settingsPane.revalidateParameters();
+ }
+ } else {
+ refreshValue();
+ }
+ }
+ }
+ });
+ /* if the TextField loses focus and is empty, set it to the current
+ * value of the parameter */
+ textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
+ @Override
+ public void changed(
+ ObservableValue<? extends Boolean> observable,
+ Boolean oldValue, Boolean newValue) {
+ if (!newValue) {
+ refreshValue();
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void setValidityStyle() {
+ // update the Control's style and tooltip based on the status of the parameter
+ if (parameter.getStatus() == ParameterStatus.INVALID) {
+ textField.setStyle(Constants.BASE_TEXT_STYLE + Constants.INVALID_PARAMETER_STYLE);
+ textField.setTooltip(tooltip);
+ tooltip.setText(parameter.getStatus().getDetails());
+ } else if (parameter.getStatus() == ParameterStatus.WARNING || parameter.getStatus() == ParameterStatus.WARNING_RESET) {
+ textField.setStyle(Constants.BASE_TEXT_STYLE + Constants.WARNING_PARAMETER_STYLE);
+ textField.setTooltip(tooltip);
+ tooltip.setText(parameter.getStatus().getDetails());
+ } else {
+ textField.setStyle(Constants.BASE_TEXT_STYLE + Constants.VALID_PARAMETER_STYLE);
+ textField.setTooltip(null);
+ }
+ }
+
+ @Override
+ public void refreshValue() {
+ if (!textField.isFocused()) {
+ textField.setText(decimalFormat.format(parameter.get().doubleValue()));
+ }
+ }
+}
diff --git a/src/jcgp/gui/settings/parameters/GUIIntegerParameter.java b/src/jcgp/gui/settings/parameters/GUIIntegerParameter.java
new file mode 100644
index 0000000..bcfbe50
--- /dev/null
+++ b/src/jcgp/gui/settings/parameters/GUIIntegerParameter.java
@@ -0,0 +1,107 @@
+package jcgp.gui.settings.parameters;
+
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.geometry.Pos;
+import javafx.scene.control.Control;
+import javafx.scene.control.TextField;
+import jcgp.backend.parameters.IntegerParameter;
+import jcgp.backend.parameters.ParameterStatus;
+import jcgp.gui.constants.Constants;
+import jcgp.gui.settings.SettingsPane;
+
+/**
+ * This extension of @code{GUIParameter} uses a @code{TextField} to display
+ * the value of a @code{IntegerParameter}. It cannot be constructed
+ * directly - instead, use @code{GUIParameter.create()}.
+ * <br><br>
+ * See {@link GUIParameter} for more information.
+ *
+ * @author Eduardo Pedroni
+ */
+public class GUIIntegerParameter extends GUIParameter<Number> {
+
+ private TextField textField;
+
+ /**
+ * This protected constructor is intended for use
+ * by the factory method only.
+ *
+ */
+ protected GUIIntegerParameter(IntegerParameter parameter, SettingsPane sp) {
+ super(parameter, sp);
+ }
+
+ @Override
+ protected Control makeControl() {
+ // this uses a text field
+ textField = new TextField(String.valueOf(parameter.get()));
+ textField.setStyle(Constants.VALID_PARAMETER_STYLE);
+ textField.setAlignment(Pos.CENTER_RIGHT);
+ textField.prefWidthProperty().bind(widthProperty().divide(2));
+
+ return textField;
+ }
+
+ @Override
+ protected void setControlListeners() {
+ /* pass the TextField value back to the parameter whenever it gets
+ * modified, provided it is not empty, the experiment isn't running
+ * and it matches the integer regex pattern */
+ textField.textProperty().addListener(new ChangeListener<String>() {
+ @Override
+ public void changed(
+ ObservableValue<? extends String> observable,
+ String oldValue, String newValue) {
+ if (!settingsPane.isExperimentRunning()) {
+ if (newValue.matches("[0-9]*")) {
+ if (!newValue.isEmpty()) {
+ int value = Integer.parseInt(newValue);
+ parameter.set(value);
+ settingsPane.revalidateParameters();
+ }
+ } else {
+ refreshValue();
+ }
+ }
+ }
+ });
+ /* if the TextField loses focus and is empty, set it to the current
+ * value of the parameter */
+ textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
+ @Override
+ public void changed(
+ ObservableValue<? extends Boolean> observable,
+ Boolean oldValue, Boolean newValue) {
+ if (!newValue) {
+ refreshValue();
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void setValidityStyle() {
+ // update the Control's style and tooltip based on the status of the parameter
+ if (parameter.getStatus() == ParameterStatus.INVALID) {
+ textField.setStyle(Constants.BASE_TEXT_STYLE + Constants.INVALID_PARAMETER_STYLE);
+ textField.setTooltip(tooltip);
+ tooltip.setText(parameter.getStatus().getDetails());
+ } else if (parameter.getStatus() == ParameterStatus.WARNING
+ || parameter.getStatus() == ParameterStatus.WARNING_RESET) {
+ textField.setStyle(Constants.BASE_TEXT_STYLE + Constants.WARNING_PARAMETER_STYLE);
+ textField.setTooltip(tooltip);
+ tooltip.setText(parameter.getStatus().getDetails());
+ } else {
+ textField.setStyle(Constants.BASE_TEXT_STYLE + Constants.VALID_PARAMETER_STYLE);
+ textField.setTooltip(null);
+ }
+ }
+
+ @Override
+ public void refreshValue() {
+ if (!textField.isFocused()) {
+ textField.setText(parameter.get().toString());
+ }
+ }
+}
diff --git a/src/jcgp/gui/settings/parameters/GUIParameter.java b/src/jcgp/gui/settings/parameters/GUIParameter.java
new file mode 100644
index 0000000..59aecf6
--- /dev/null
+++ b/src/jcgp/gui/settings/parameters/GUIParameter.java
@@ -0,0 +1,235 @@
+package jcgp.gui.settings.parameters;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javafx.application.Platform;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.geometry.Pos;
+import javafx.scene.control.Control;
+import javafx.scene.control.Label;
+import javafx.scene.control.Tooltip;
+import javafx.scene.layout.HBox;
+import jcgp.backend.parameters.BooleanParameter;
+import jcgp.backend.parameters.DoubleParameter;
+import jcgp.backend.parameters.IntegerParameter;
+import jcgp.backend.parameters.Parameter;
+import jcgp.backend.parameters.ParameterStatus;
+import jcgp.gui.settings.SettingsPane;
+
+/**
+ *
+ * This is the base class for all @code{GUIParameter}s. Using the factory method @code{GUIParameter.create()}
+ * generates an appropriate instance of this class for the specified parameter.
+ * <br><br>
+ * A @code{GUIParameter} is an @code{HBox} containing a @code{Text} for the parameter name
+ * and a @code{Control} for interaction.
+ * It stores an instance of its associated @code{Parameter} object and also contains a @code{Tooltip} for
+ * displaying status information.
+ * <br><br>
+ * Monitor parameters are updated automatically and have their @code{Control} disabled so
+ * that no changes can be made via the GUI.
+ * Non-monitor parameters are updated automatically as well, but may be changed by the user
+ * if the program is not evolving.
+ *
+ * @see Parameter
+ * @author Eduardo Pedroni
+ * @param <T> the parameter data type
+ */
+public abstract class GUIParameter<T> extends HBox {
+
+ private Label name;
+ private Control valueControl;
+
+ protected SettingsPane settingsPane;
+ protected Tooltip tooltip;
+ protected Parameter<T> parameter;
+
+ /** This is the lock used to prevent more than one update task to be scheduled
+ * at the same time on the same GUIParameter. */
+ private AtomicBoolean updateLock = new AtomicBoolean(false);
+
+ /**
+ * This value is used to assert whether the control has changed values since
+ * the program last ran. Therefore, it is updated whenever a generation occurs
+ * or the experiment is reset.
+ */
+ private T referenceValue;
+
+ /**
+ * This protected template constructor contains the common elements to all
+ * @code{GUIParameter}s and should be invoked by any subclasses using @code{super()}. It
+ * defers the creation of the parameter {@code Control} object to the subclass
+ * currently being built (which in turn is defined by the factory method).
+ *
+ * @param parameter a @code{Parameter} for which to generate a @code{GUIParameter}.
+ * @param sp a reference to the @code{SettingsPane}.
+ */
+ protected GUIParameter(Parameter<T> parameter, final SettingsPane settingsPane) {
+ this.parameter = parameter;
+ this.referenceValue = parameter.get();
+
+ this.settingsPane = settingsPane;
+
+ setAlignment(Pos.CENTER_LEFT);
+ setSpacing(5);
+
+ name = new Label(parameter.toString());
+ // set text width to half of the total width of the GUIParameter
+ name.prefWidthProperty().bind(widthProperty().divide(2));
+
+ // the tooltip is the hover-over label containing status information, when appropriate
+ tooltip = new Tooltip();
+ tooltip.setSkin(null);
+
+ valueControl = makeControl();
+
+ // if the parameter is a monitor, it should be permanently disabled
+ valueControl.setDisable(parameter.isMonitor());
+
+ // bind to parameter value property in a thread-safe way
+ makeThreadSafeBinding();
+
+ // if parameter is not a monitor, make sure the control is constrained appropriately
+ if (!parameter.isMonitor()) {
+ setControlListeners();
+ }
+
+ getChildren().addAll(name, valueControl);
+ }
+
+ /**
+ * Factory method to create @code{GUIParameter}s from @code{Parameter}s.
+ * Use this to create an appropriate @code{GUIParameter} from any instance of @code{Parameter},
+ * rather than manually downcasting the @code{Parameter} object every time.
+ *
+ * @param parameter a parameter for which to generate a @code{GUIParameter}.
+ * @param sp a reference to the @code{SettingsPane}.
+ * @return an appropriate instance of @code{GUIParameter}.
+ */
+ public static GUIParameter<?> create(Parameter<?> parameter, SettingsPane sp) {
+ if (parameter instanceof IntegerParameter) {
+ return new GUIIntegerParameter((IntegerParameter) parameter, sp);
+ } else if (parameter instanceof DoubleParameter) {
+ return new GUIDoubleParameter((DoubleParameter) parameter, sp);
+ } else if (parameter instanceof BooleanParameter) {
+ return new GUIBooleanParameter((BooleanParameter) parameter, sp);
+ } else {
+ throw new ClassCastException("No GUIParameter subclass exists for argument of type " + parameter.getClass());
+ }
+ }
+
+ /**
+ * Parameters are intended to communicate information from the experiment
+ * to the GUI. Since the experiment runs on a separate threads and it is illegal
+ * to modify JavaFX objects from outside the JavaFX Application thread, this
+ * special ChangeListener updates the GUIParameter in a safe way.
+ * <br><br>
+ * Note that this is applied to all parameters regardless of whether they are
+ * monitors or not; the only difference between monitor and non-monitor parameters
+ * is that monitor parameters cannot be modified from the GUI.
+ */
+ private void makeThreadSafeBinding() {
+ parameter.valueProperty().addListener(new ChangeListener<Object>() {
+ @Override
+ public void changed(
+ ObservableValue<? extends Object> observable,
+ Object oldValue, Object newValue) {
+ // only do this if the experiment is running
+ if (settingsPane.isExperimentRunning() || !isFocused()) {
+ /* here's the catch - atomically get the lock state and set it to true
+ * the lock will only be false again when the runnable is finished executing,
+ * preventing multiple runnables to concurrently update the same GUIParameter
+ */
+ if (!updateLock.getAndSet(true)) {
+ Platform.runLater(new Runnable() {
+ @Override
+ public void run() {
+ refreshValue();
+ updateLock.set(false);
+ }
+ });
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * @return true if the current value of the parameter does not prevent the
+ * experiment from running.
+ */
+ public boolean isValid() {
+ return parameter.getStatus() != ParameterStatus.INVALID;
+ }
+
+ /**
+ * Force the parameter to validate its current value, and apply the associated
+ * style to the @code{GUIParameter}.
+ */
+ public void validate() {
+ parameter.validate(parameter.get());
+ setValidityStyle();
+ }
+
+ /**
+ * Certain parameter changes might require the experiment to be reset, either
+ * because the parameter is critical or because its status requires a reset.
+ *
+ * @return true if an experiment reset is required due to this parameter changing.
+ */
+ public boolean requiresReset() {
+ return (parameter.isCritical() && !parameter.get().equals(referenceValue))
+ || parameter.getStatus() == ParameterStatus.WARNING_RESET;
+ }
+
+ /**
+ * Set the current parameter value as the reference value of the @code{GUIParameter}.
+ * The new reference value will be used to determine the validity of the parameter,
+ * should its value change.
+ */
+ public void applyValue() {
+ referenceValue = parameter.get();
+ }
+
+ /*
+ * The following prototypes are instance-dependent and are called from
+ * GUIParameter() as necessary.
+ */
+ /**
+ * This method returns the @code{Control} object used to control the parameter.
+ * <br><br>
+ * Implementations of @code{GUIParameter} must override this method and return
+ * a @code{Control} appropriate to the type of parameter. This will typically be
+ * done by referencing the protected field @code{GUIParameter.parameter}.
+ *
+ * @return the Control object to be added to the GUIParameter.
+ */
+ protected abstract Control makeControl();
+
+ /**
+ * Adds the necessary handlers to the @code{Control} object in order to modify
+ * the underlying parameter. This will typically consist of filtering key
+ * presses to ensure no invalid characters are inserted, applying the new
+ * value to the underlying parameter and revalidating the parameters to
+ * reflect the changes made.
+ */
+ protected abstract void setControlListeners();
+
+ /**
+ * This method is called to style the @code{GUIParameter} according to the status of
+ * the parameter, which can be obtained with @code{parameter.getStatus()}. While the
+ * subclass is free to style itself in any way, the CSS strings defined here
+ * (INVALID_PARAMETER_STYLE, WARNING_PARAMETER_STYLE, VALID_PARAMETER_STYLE)
+ * provide a way to keep the GUI consistent.
+ *
+ * @see ParameterStatus
+ */
+ protected abstract void setValidityStyle();
+
+ /**
+ * Update the control so it shows the correct value of the parameter. This method
+ * is used exclusively by the thread-safe binding created if the module is a monitor.
+ */
+ protected abstract void refreshValue();
+}
diff --git a/src/jcgp/gui/settings/testcase/TestCaseTable.java b/src/jcgp/gui/settings/testcase/TestCaseTable.java
new file mode 100644
index 0000000..605b75e
--- /dev/null
+++ b/src/jcgp/gui/settings/testcase/TestCaseTable.java
@@ -0,0 +1,124 @@
+package jcgp.gui.settings.testcase;
+
+import java.util.ArrayList;
+
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ObservableList;
+import javafx.event.EventHandler;
+import javafx.scene.Scene;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableColumn.CellDataFeatures;
+import javafx.scene.control.TableView;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+import javafx.util.Callback;
+import jcgp.backend.modules.problem.TestCaseProblem;
+import jcgp.backend.modules.problem.TestCaseProblem.TestCase;
+import jcgp.backend.resources.Resources;
+import jcgp.gui.GUI;
+
+/**
+ * This is a test case table. For problems that have test cases,
+ * this table shows the test case inputs and outputs. Clicking on
+ * a test case (one is shown per row) applies the values to all
+ * chromosome inputs shows the calculated values throughout the chromosome.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public class TestCaseTable extends Stage {
+
+ private TableView<TestCase<Object>> table;
+
+ /**
+ * Make a new instance of {@code TestCaseTable}.
+ *
+ * @param testCaseProblem the {@code TestCaseProblem} whose data must be displayed.
+ * @param gui a reference to the GUI.
+ */
+ public TestCaseTable(final TestCaseProblem<Object> testCaseProblem, final GUI gui) {
+ super();
+
+ Resources resources = gui.getExperiment().getResources();
+
+ // create the actual table view
+ table = new TableView<TestCase<Object>>();
+ // get test cases from problem
+ ObservableList<TestCase<Object>> testCaseList = testCaseProblem.getTestCases();
+
+ // prepare input and output columns
+ ArrayList<TableColumn<TestCase<Object>, String>> inputs = new ArrayList<TableColumn<TestCase<Object>, String>>(resources.inputs());
+ ArrayList<TableColumn<TestCase<Object>, String>> outputs = new ArrayList<TableColumn<TestCase<Object>, String>>(resources.outputs());
+
+ // create input columns
+ TableColumn<TestCase<Object>, String> tc;
+ for (int i = 0; i < resources.inputs(); i++) {
+ tc = new TableColumn<TestCase<Object>, String>("I: " + i);
+ inputs.add(tc);
+ final int index = i;
+ tc.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<TestCase<Object>,String>, ObservableValue<String>>() {
+ @Override
+ public ObservableValue<String> call(CellDataFeatures<TestCase<Object>, String> param) {
+ // create a new string property and give it the test case value, no need for dynamic binding - this wont change often
+ return new SimpleStringProperty(param.getValue().getInputs()[index].toString());
+ }
+ });
+ tc.setSortable(false);
+ // set column width so all columns are distributed across the width of the stage
+ tc.prefWidthProperty().bind(table.widthProperty().divide(resources.inputs() + resources.outputs()));
+ }
+
+ // create output columns
+ for (int o = 0; o < resources.outputs(); o++) {
+ tc = new TableColumn<TestCase<Object>, String>("O: " + o);
+ outputs.add(tc);
+ final int index = o;
+ tc.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<TestCase<Object>,String>, ObservableValue<String>>() {
+ @Override
+ public ObservableValue<String> call(CellDataFeatures<TestCase<Object>, String> param) {
+ // create a new string property and give it the test case value, no need for dynamic binding - this wont change often
+ return new SimpleStringProperty(param.getValue().getOutputs()[index].toString());
+ }
+ });
+ tc.setSortable(false);
+ // set column width so all columns are distributed across the width of the stage
+ tc.prefWidthProperty().bind(table.widthProperty().divide(resources.inputs() + resources.outputs()));
+ }
+
+ // add created columns
+ table.getColumns().addAll(inputs);
+ table.getColumns().addAll(outputs);
+
+ // populate table with actual data
+ table.setItems(testCaseList);
+
+ // apply test case values when a new test case is selected
+ table.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<TestCase<Object>>() {
+ @Override
+ public void changed(ObservableValue<? extends TestCase<Object>> observable, TestCase<Object> oldValue, TestCase<Object> newValue) {
+ gui.evaluateTestCase(newValue);
+ }
+ });
+
+ // when the stage is closed, clear the selection
+ // this doesn't work if the stage is closed by the program for some reason...
+ setOnCloseRequest(new EventHandler<WindowEvent>() {
+ @Override
+ public void handle(WindowEvent event) {
+ gui.hideGeneValues();
+ table.getSelectionModel().clearSelection();
+ }
+ });
+
+ setScene(new Scene(table));
+ }
+
+ /**
+ * @return a reference to the actual table of test cases.
+ */
+ public TableView<TestCase<Object>> getTable() {
+ return table;
+ }
+}