package jcgp.gui.population; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.EventHandler; import javafx.geometry.VPos; import javafx.scene.control.Label; import javafx.scene.input.MouseDragEvent; import javafx.scene.input.MouseEvent; import javafx.scene.paint.Paint; import javafx.scene.shape.Circle; import javafx.scene.shape.Line; import javafx.scene.text.Font; import javafx.scene.text.Text; import javafx.scene.text.TextAlignment; import jcgp.backend.population.Connection; import jcgp.backend.population.Input; import jcgp.backend.population.Node; import jcgp.gui.ChromosomePane; import jcgp.gui.GUI; public class GUINode extends GUIGene { private Line[] lines; private Node node; private int connectionIndex = 0; public GUINode(ChromosomePane parentRef, final Node node, Line[] connectionLines) { // store references this.parent = parentRef; this.node = node; this.lines = connectionLines; // move the GUIGene to the right position relocate(((node.getColumn() + 1) * (2 * NODE_RADIUS + SPACING)) + NODE_RADIUS, (node.getRow() * (2 * NODE_RADIUS + SPACING)) + NODE_RADIUS); // set the line ends correctly updateLines(); final Label connectionNumber = new Label(); connectionNumber.setStyle("-fx-background-color:rgb(255, 255, 255); -fx-border-color:rgba(0, 0, 0, 0.5); "); connectionNumber.setVisible(false); Circle output = new Circle(NODE_RADIUS, 0, SOCKET_RADIUS, Paint.valueOf("white")); output.setStroke(Paint.valueOf("black")); mainCircle = new Circle(NODE_RADIUS, Paint.valueOf("white")); mainCircle.setStroke(Paint.valueOf("black")); text = new Text(node.getFunction().getName()); text.setFont(Font.font("Arial", NODE_TEXT)); text.setTextOrigin(VPos.CENTER); text.setTextAlignment(TextAlignment.CENTER); text.setWrappingWidth(NODE_RADIUS * 2); text.setX(-NODE_RADIUS); text.setVisible(true); Circle[] sockets = new Circle[GUI.resources.getInt("arity")]; double angle, xPos, yPos; for (int l = 0; l < sockets.length; l++) { angle = ((((double) (l + 1)) / ((GUI.resources.getDouble("arity") + 1))) * THETA) - (THETA / 2); xPos = -Math.cos(angle) * NODE_RADIUS; yPos = Math.sin(angle) * NODE_RADIUS; sockets[l] = new Circle(xPos, yPos, GUIGene.SOCKET_RADIUS, Paint.valueOf("white")); sockets[l].setId(String.valueOf(l)); sockets[l].setStroke(Paint.valueOf("black")); final Circle s = sockets[l]; final int index = l; /* * Mouse event handlers on sockets * */ s.addEventFilter(MouseDragEvent.DRAG_DETECTED, new EventHandler() { @Override public void handle(MouseEvent event) { // the mouse has been dragged out of the socket, this means a full drag is in progress startFullDrag(); } }); s.addEventFilter(MouseEvent.MOUSE_ENTERED, new EventHandler() { @Override public void handle(MouseEvent event) { // user is hovering over connection socket connectionNumber.setText("C: " + s.getId()); connectionNumber.relocate(s.getCenterX() + 5, s.getCenterY() - 10); connectionNumber.setVisible(true); } }); s.addEventFilter(MouseEvent.MOUSE_EXITED, new EventHandler() { @Override public void handle(MouseEvent event) { // user exits the connection socket connectionNumber.setVisible(false); } }); s.addEventFilter(MouseDragEvent.MOUSE_PRESSED, new EventHandler() { @Override public void handle(MouseEvent event) { // mouse was pressed on the socket stateProperty.set(GUIGeneState.SOURCE); connectionIndex = index; } }); s.addEventFilter(MouseDragEvent.MOUSE_DRAGGED, new EventHandler() { @Override public void handle(MouseEvent event) { if (!parent.isTarget()) { lines[connectionIndex].setEndX(event.getX() + ((Circle) event.getSource()).getParent().getLayoutX()); lines[connectionIndex].setEndY(event.getY() + ((Circle) event.getSource()).getParent().getLayoutY()); } } }); s.addEventFilter(MouseDragEvent.MOUSE_RELEASED, new EventHandler() { @Override public void handle(MouseEvent event) { if (event.isStillSincePress()) { // mouse was released before dragging out of the socket updateLine(index); stateProperty.set(GUIGeneState.HOVER); } else if (stateProperty.get() == GUIGeneState.SOURCE) { // no connection has been made, fallback resetState(); updateLines(); } } }); } /* * Mouse event handlers on whole gene */ addEventFilter(MouseDragEvent.MOUSE_DRAG_ENTERED, new EventHandler() { @Override public void handle(MouseDragEvent event) { // the drag has entered this node, react appropriately // this happens even if we are the source of the drag if (isAllowed((GUIGene) event.getGestureSource(), (GUIGene) event.getSource())) { ((GUIGene) event.getGestureSource()).setConnectionLine((GUIGene) event.getSource()); Connection source = ((GUIGene) event.getGestureSource()).getChangingConnection(); if (node == source) { stateProperty.set(GUIGeneState.NO_CHANGE_TARGET); } else { stateProperty.set(GUIGeneState.TARGET); } } else { stateProperty.set(GUIGeneState.FORBIDDEN_TARGET); } } }); addEventFilter(MouseDragEvent.MOUSE_DRAG_EXITED, new EventHandler() { @Override public void handle(MouseDragEvent event) { // the drag has exited this node, react appropriately // this happens even if we are the source of the drag parent.setTarget(false); if (event.isPrimaryButtonDown()) { if (event.getGestureSource() == event.getSource()) { stateProperty.set(GUIGeneState.SOURCE); } else { if (stateProperty.get() == GUIGeneState.NO_CHANGE_TARGET) { stateProperty.set(GUIGeneState.INDIRECT_HOVER); } else { stateProperty.set(GUIGeneState.NEUTRAL); ((GUIGene) event.getGestureSource()).setConnections(GUIGeneState.INDIRECT_HOVER); } } } } }); addEventFilter(MouseDragEvent.MOUSE_DRAG_RELEASED, new EventHandler() { @Override public void handle(MouseDragEvent event) { GUIGene source = ((GUIGene) event.getGestureSource()); // set states to reflect the new situation if (source.isLocked()) { source.setState(GUIGeneState.HOVER); source.setConnections(GUIGeneState.HOVER); } else { source.setState(GUIGeneState.NEUTRAL); source.setConnections(GUIGeneState.NEUTRAL); } // the user released the drag gesture on this node, react appropriately if (isAllowed((GUIGene) event.getGestureSource(), (GUIGene) event.getSource())) { if (source.isLocked()) { // remove locks from the old connection, add the to the new // note that the old connection may still have locks after this parent.getGuiGene(source.getChangingConnection()).removeLocks(source.getLocks()); source.setChangingConnection(node); addLocks(source.getLocks()); } else { if (source instanceof GUIOutput) { source.resetState(); } source.setChangingConnection(node); } } source.updateLines(); stateProperty.set(GUIGeneState.HOVER); } }); addEventFilter(MouseEvent.MOUSE_ENTERED, new EventHandler() { @Override public void handle(MouseEvent event) { // cursor has entered this node without dragging, or it is dragging and this is the source if (stateProperty.get() == GUIGeneState.NEUTRAL) { stateProperty.set(GUIGeneState.HOVER); } else if (locked > 0) { setConnections(GUIGeneState.LOCKED_HOVER); } } }); addEventFilter(MouseEvent.MOUSE_EXITED, new EventHandler() { @Override public void handle(MouseEvent event) { // cursor has left this node without dragging, or it is dragging and this is the source if (stateProperty.get() == GUIGeneState.HOVER && locked <= 0) { stateProperty.set(GUIGeneState.NEUTRAL); setConnections(GUIGeneState.NEUTRAL); } else if (locked > 0) { if (stateProperty.get() == GUIGeneState.SOURCE || stateProperty.get() == GUIGeneState.FORBIDDEN_TARGET) { setConnections(GUIGeneState.INDIRECT_HOVER); } else { setConnections(GUIGeneState.HOVER); } } } }); getChildren().addAll(mainCircle, text); getChildren().addAll(sockets); getChildren().addAll(output, connectionNumber); stateProperty.addListener(new ChangeListener() { @Override public void changed(ObservableValue observable, GUIGeneState oldValue, GUIGeneState newValue) { switch (newValue) { case ACTIVE_HOVER: if (locked > 0) { stateProperty().set(GUIGeneState.LOCKED_HOVER); } else { mainCircle.setFill(Paint.valueOf(GUI.SOFT_HIGHLIGHT_COLOUR)); showLines(true); } setConnections(GUIGeneState.ACTIVE_HOVER); break; case LOCKED_HOVER: mainCircle.setFill(Paint.valueOf(GUI.SOFT_HIGHLIGHT_COLOUR)); break; case FORBIDDEN_TARGET: mainCircle.setFill(Paint.valueOf(GUI.BAD_SELECTION_COLOUR)); break; case HOVER: mainCircle.setFill(Paint.valueOf(GUI.MEDIUM_HIGHLIGHT_COLOUR)); showLines(true); if (locked <= 0) { setConnections(GUIGeneState.INDIRECT_HOVER); } else { setConnections(GUIGeneState.HOVER); } break; case INDIRECT_HOVER: mainCircle.setFill(Paint.valueOf(GUI.SOFT_HIGHLIGHT_COLOUR)); break; case NEUTRAL: if (locked > 0) { stateProperty.set(GUIGeneState.HOVER); } else { mainCircle.setFill(Paint.valueOf(GUI.NEUTRAL_COLOUR)); showLines(false); if (oldValue == GUIGeneState.ACTIVE_HOVER) { setConnections(GUIGeneState.NEUTRAL); } } break; case NO_CHANGE_TARGET: parent.setTarget(true); mainCircle.setFill(Paint.valueOf(GUI.NEUTRAL_SELECTION_COLOUR)); break; case SOURCE: mainCircle.setFill(Paint.valueOf(GUI.HARD_HIGHLIGHT_COLOUR)); break; case TARGET: parent.setTarget(true); mainCircle.setFill(Paint.valueOf(GUI.GOOD_SELECTION_COLOUR)); break; default: break; } } }); // for (int c = 0; c < lines.length; c++) { // final int i = c; // node.connections().get(c).addListener(new ChangeListener() { // @Override // public void changed(ObservableValue observable, // Connection oldValue, Connection newValue) { // updateLine(i); // } // }); // } } @Override public Connection getChangingConnection() { return node.getConnection(connectionIndex); } private boolean isAllowed(GUIGene source, GUIGene 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).getGene(), s = ((GUINode) source).getGene(); if (s.getColumn() - t.getColumn() > 0 && s.getColumn() - t.getColumn() <= GUI.resources.getInt("levelsBack")) { return true; } return false; } else if (target instanceof GUIOutput) { return false; } else { throw new ClassCastException("Target was neither GUINode nor GUIInput nor GUIOutput."); } } 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 if (target instanceof GUIOutput) { return false; } else { throw new ClassCastException("Target was neither GUINode nor GUIInput nor GUIOutput."); } } // if the source was neither node nor output, something bad is happening throw new ClassCastException("Source was neither GUINode nor GUIOutput."); } /** * Place the end of the specified line on the output of the associated connection. * * @param index the line to be updated. */ public void updateLine(int index) { if (node.getConnection(index) instanceof Node) { int row = ((Node) node.getConnection(index)).getRow(), column = ((Node) node.getConnection(index)).getColumn(); lines[index].setEndX(((column + 1) * (2 * NODE_RADIUS + SPACING)) + 2 * NODE_RADIUS); lines[index].setEndY((row * (2 * NODE_RADIUS + SPACING)) + NODE_RADIUS); } else if (node.getConnection(index) instanceof Input) { int inputIndex = ((Input) node.getConnection(index)).getIndex(); lines[index].setEndX(2 * NODE_RADIUS); lines[index].setEndY(inputIndex * (2 * NODE_RADIUS + SPACING) + NODE_RADIUS); } } /** * Updates the end of all lines to match the associated connections. */ public void updateLines() { for (int c = 0; c < lines.length; c++) { updateLine(c); } } /** * Toggle visibility of all connection lines. * * @param value whether to show the lines or not. */ private void showLines(boolean value) { for (int i = 0; i < lines.length; i++) { lines[i].setVisible(value); } } @Override public Node getGene() { return node; } @Override public void setConnections(GUIGeneState newState) { for (int i = 0; i < lines.length; i++) { parent.getGuiGene(node.getConnection(i)).setState(newState); } } @Override public void setChangingConnection(Connection newConnection) { node.setConnection(connectionIndex, newConnection); } @Override public void resetState() { if (locked > 0) { stateProperty.set(GUIGeneState.HOVER); } else { stateProperty.set(GUIGeneState.NEUTRAL); setConnections(GUIGeneState.NEUTRAL); } } @Override void setLocked(boolean value) { locked += value ? 1 : -1; stateProperty.set(locked > 0 ? GUIGeneState.HOVER : GUIGeneState.ACTIVE_HOVER); for (int i = 0; i < lines.length; i++) { parent.getGuiGene(node.getConnection(i)).setLocked(value); } } @Override public void addLocks(int value) { locked += value; stateProperty.set(locked > 0 ? GUIGeneState.HOVER : GUIGeneState.ACTIVE_HOVER); for (int i = 0; i < lines.length; i++) { parent.getGuiGene(node.getConnection(i)).addLocks(value); } } @Override public void removeLocks(int value) { locked -= value; stateProperty.set(locked > 0 ? GUIGeneState.HOVER : GUIGeneState.NEUTRAL); for (int i = 0; i < lines.length; i++) { parent.getGuiGene(node.getConnection(i)).removeLocks(value); } } @Override public void setConnectionLine(GUIGene gene) { lines[connectionIndex].setEndX(gene.getLayoutX() + NODE_RADIUS); lines[connectionIndex].setEndY(gene.getLayoutY()); } public void updateFunction() { text.setText(node.getFunction().getName()); } }