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 | |
| parent | 9062115b7d15cb05552632dc5486a5cd15a45289 (diff) | |
Added existing source code
Diffstat (limited to 'src')
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; +	} +} | 
