diff options
author | Eduardo Pedroni <e.pedroni91@gmail.com> | 2015-03-09 17:03:48 -0300 |
---|---|---|
committer | Eduardo Pedroni <e.pedroni91@gmail.com> | 2015-03-09 17:03:48 -0300 |
commit | d69fa8746728367646494fd8c2c18944f306c6a2 (patch) | |
tree | f063f9efc6c93a5520991f509f3a481543b73a91 /src/jcgp | |
parent | 9062115b7d15cb05552632dc5486a5cd15a45289 (diff) |
Added existing source code
Diffstat (limited to 'src/jcgp')
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; + } +} |