This is an incomplete implementation of something similar to scratch. It's incomplete because right now, I'm working purely on the user interface.
I used Swing to make the basics of it, but then hacked my own code for the rest of it, because I wasn't sure if Swing could give me what I wanted. Basically, I wanted to be able to drag around the pieces (which I could do with Swing) but also drag the background around.
I'm looking for a way to improve this code and also whether I took the right approach or whether I should rewrite it in Swing (and how).
MainPanel.java
public class MainPanel extends JPanel {
private static final int SEPARATOR_DISTANCE = 100;
private static final int NUMBER_DISTANCE = 200;
private final List<Piece> pieces = new ArrayList<>();
private int x;
private int y;
public MainPanel() {
super();
pieces.add(new NumberConstant(5, 100, 10));
x = y = 0;
final MainInputHandler input = new MainInputHandler(this);
addMouseListener(input);
addMouseMotionListener(input);
}
@Override
protected void paintComponent(final Graphics g) {
GraphicsUtils.prettyGraphics((Graphics2D) g);
super.paintComponent(g);
g.setColor(GraphicsConstants.MAIN_BACKROUND_COLOR);
g.fillRect(0, 0, getWidth(), getHeight());
drawGrid(g);
g.translate(-x, -y);
for (final Piece p : pieces) {
p.draw((Graphics2D) g);
}
}
private void drawGrid(final Graphics g) {
final int textSpaceBuffer = 5;
g.setColor(GraphicsConstants.MAIN_GRID_COLOR);
// Draws vertical lines
for (int sepX = -(x % SEPARATOR_DISTANCE) - SEPARATOR_DISTANCE; sepX < getWidth(); sepX += SEPARATOR_DISTANCE) {
if (sepX + x == 0) {
g.setColor(GraphicsConstants.MAIN_GRID_ORIGIN_COLOR);
} else {
g.setColor(GraphicsConstants.MAIN_GRID_COLOR);
}
g.drawLine(sepX, 0, sepX, getHeight());
// draws line coordinates if neccesary
if ((sepX + x) % NUMBER_DISTANCE == 0) {
g.setColor(GraphicsConstants.MAIN_GRID_COLOR);
g.drawString(String.valueOf(sepX + x), sepX + textSpaceBuffer, g.getFontMetrics().getMaxAscent() + textSpaceBuffer);
}
}
// draws horizontal lines
for (int sepY = -(y % SEPARATOR_DISTANCE); sepY < getHeight() + SEPARATOR_DISTANCE; sepY += SEPARATOR_DISTANCE) {
if (sepY + y == 0) {
g.setColor(GraphicsConstants.MAIN_GRID_ORIGIN_COLOR);
} else {
g.setColor(GraphicsConstants.MAIN_GRID_COLOR);
}
g.drawLine(0, sepY, getWidth(), sepY);
// draws line coordinates if necessary
if ((sepY + y) % NUMBER_DISTANCE == 0) {
g.setColor(GraphicsConstants.MAIN_GRID_COLOR);
g.drawString(String.valueOf(sepY + y), textSpaceBuffer, sepY - textSpaceBuffer);
}
}
}
public void centerOnOrigin() {
x = -getWidth() / 2;
y = -getHeight() / 2;
}
public void setSpacePosition(final int x, final int y) {
this.x = x;
this.y = y;
}
public int getSpaceX() {
return x;
}
public int getSpaceY() {
return y;
}
public Point getSpacePosition() {
return new Point(x, y);
}
public Point getWorldCoordFromMouse(final Point p) {
return new Point(x + p.x, y + p.y);
}
public List<Piece> getPieces() {
return pieces;
}
}
Piece.java
/**
* Abstract class. To implement this class, you must add a method with the signature<br>
* <code>public static String name()</code>
* */
public abstract class Piece {
public static final String MAX_LENGTH_STRING;
private static final int PORT_SIZE = 20;
private static final int GAP_SIZE = 10;
private static final int BORDER_SPACE = 5;
private static List<Class<? extends Piece>> pieces = new ArrayList<>();
private static Map<Class<? extends Piece>, String> pieceNames = new HashMap<>();
private final ProgramValue[] inputs;
private final Connection[] outputs;
private int x;
private int y;
private FontMetrics fontMetrics;
static {
addPiece(NumberConstant.class);
String longestString = "";
final Iterator<String> it = getPieceNames().values().iterator();
while (it.hasNext()) {
final String next = it.next();
if (next.length() > longestString.length()) {
longestString = next;
}
}
MAX_LENGTH_STRING = longestString;
}
protected Piece(final int inputs, final int outputs, final int x, final int y) {
this.inputs = new ProgramValue[inputs];
this.outputs = new Connection[outputs];
for (int i = 0; i < outputs; i++) {
getOutputs()[i] = new Connection(this, i, null, 0);
}
this.x = x;
this.y = y;
}
// Piece should take inputs and figure out its output
public abstract void update(ProgramContext pc);
/**
* Assumes that this should draw at (0,0)
* */
public void draw(final Graphics2D g) {
g.translate(x, y);
g.setColor(GraphicsConstants.PIECE_BACKGROUND);
final String name = pieceNames.get(getClass());
// Store this variable so other methods can use it without accessing graphics
fontMetrics = g.getFontMetrics();
final int nameWidth = getNameWidth(name);
g.fill(getBodyShape(nameWidth));
final int nameHeight = fontMetrics.getMaxAscent();
g.setColor(GraphicsConstants.PIECE_TEXT);
g.drawString(name, BORDER_SPACE, nameHeight);
for (int i = 0; i < inputs.length; i++) {
g.drawOval(BORDER_SPACE, nameHeight + GAP_SIZE + (PORT_SIZE + GAP_SIZE) * i, PORT_SIZE, PORT_SIZE);
}
for (int i = 0; i < outputs.length; i++) {
g.drawOval(nameWidth - PORT_SIZE - BORDER_SPACE, nameHeight + GAP_SIZE + (PORT_SIZE + GAP_SIZE) * i, PORT_SIZE, PORT_SIZE);
}
}
/**
* @param a
* point in world space
* @return the connection that was clicked on
* */
public Optional<Connection> outputPortContainingPoint(final Point worldCoord) {
final Point worldCoordCopy = new Point(worldCoord);
// the body shape is at 0,0 so we have to translate that by its x and y OR translate our point by -x and -y
worldCoordCopy.translate(-x, -y);
final int nameWidth = getNameWidth(pieceNames.get(getClass()));
final int nameHeight = fontMetrics.getMaxAscent();
for (int i = 0; i < outputs.length; i++) {
if (new Ellipse2D.Float(nameWidth - PORT_SIZE - BORDER_SPACE, nameHeight + GAP_SIZE + (PORT_SIZE + GAP_SIZE) * i, PORT_SIZE, PORT_SIZE).contains(worldCoordCopy)) {
return Optional.of(outputs[i]);
}
}
return Optional.empty();
}
private int getNameWidth(final String name) {
return (int) (fontMetrics.stringWidth(name) * 1.5);
}
public static List<Class<? extends Piece>> values() {
return pieces;
}
private static void addPiece(final Class<? extends Piece> p) {
pieces.add(p);
try {
// Assumes subclass has a static method called name
getPieceNames().put(p, p.getMethod("name").invoke(null).toString());
} catch (final NoSuchMethodException e) {
e.printStackTrace();
// if they didn't supply a name method, use the class name instead
getPieceNames().put(p, p.getSimpleName());
} catch (final SecurityException e) {
e.printStackTrace();
} catch (final IllegalAccessException e) {
e.printStackTrace();
} catch (final IllegalArgumentException e) {
e.printStackTrace();
} catch (final InvocationTargetException e) {
e.printStackTrace();
}
}
private RoundRectangle2D getBodyShape(final int nameWidth) {
final int curve = 5;
final int height = fontMetrics.getMaxAscent() + GAP_SIZE + (PORT_SIZE + GAP_SIZE) * Math.max(inputs.length, outputs.length);
return new RoundRectangle2D.Float(0, 0, nameWidth, height, curve, curve);
}
public boolean containsPoint(final Point worldCoord) {
final Point worldCoordCopy = new Point(worldCoord);
// the body shape is at 0,0 so we have to translate that by its x and y OR translate our point by -x and -y
worldCoordCopy.translate(-x, -y);
return getBodyShape(getNameWidth(pieceNames.get(getClass()))).contains(worldCoordCopy);
}
public static Map<Class<? extends Piece>, String> getPieceNames() {
return pieceNames;
}
public void changeInput(final int inputPort, final ProgramValue value) {
assert value != null;
inputs[inputPort] = value;
}
protected Connection[] getOutputs() {
return outputs;
}
public void setPosition(final int x, final int y) {
this.x = x;
this.y = y;
}
public Point getPosition() {
return new Point(x, y);
}
}
MainInputHandler.java
public class MainInputHandler implements MouseListener, MouseMotionListener {
private final MainPanel mainPanel;
private Optional<Point> pressedPosition = Optional.empty();
private Optional<Point> initialPosition = Optional.empty();
private Optional<Piece> pieceDragged = Optional.empty();
private Optional<Point> pieceInitialPosition = Optional.empty();
private Optional<Connection> portSelected = Optional.empty();
public MainInputHandler(final MainPanel mainPanel) {
this.mainPanel = mainPanel;
}
@Override
public void mousePressed(final MouseEvent e) {
final List<Piece> collidingPieces = new ArrayList<>(mainPanel.getPieces());
final Point worldCoord = mainPanel.getWorldCoordFromMouse(e.getPoint());
collidingPieces.removeIf((final Piece p) -> !p.containsPoint(worldCoord));
if (!collidingPieces.isEmpty()) {
// we are clicking on one or more pieces, drag the top piece (last in the list)
final Piece selected = collidingPieces.get(collidingPieces.size() - 1);
final Optional<Connection> outputPortSelected = selected.outputPortContainingPoint(worldCoord);
if (outputPortSelected.isPresent()) {
portSelected = outputPortSelected;
} else {
// we didn't select a port, we selected th body
pieceDragged = Optional.of(selected);
pieceInitialPosition = Optional.of(pieceDragged.get().getPosition());
}
} else {
pieceDragged = Optional.empty();
pieceInitialPosition = Optional.empty();
}
pressedPosition = Optional.of(e.getPoint());
initialPosition = Optional.of(mainPanel.getSpacePosition());
}
@Override
public void mouseReleased(final MouseEvent e) {
pressedPosition = Optional.empty();
initialPosition = Optional.empty();
pieceDragged = Optional.empty();
pieceInitialPosition = Optional.empty();
portSelected = Optional.empty();
}
@Override
public void mouseDragged(final MouseEvent e) {
if (portSelected.isPresent()) {
// Drag a connection
// TODO
} else if (pieceDragged.isPresent() && pieceInitialPosition.isPresent()) {
// Drag a piece
final int x = pieceInitialPosition.get().x + e.getPoint().x - pressedPosition.get().x;
final int y = pieceInitialPosition.get().y + e.getPoint().y - pressedPosition.get().y;
pieceDragged.get().setPosition(x, y);
mainPanel.repaint();
} else if (pressedPosition.isPresent() && initialPosition.isPresent()) {
// Move the background
final int x = initialPosition.get().x + pressedPosition.get().x - e.getPoint().x;
final int y = initialPosition.get().y + pressedPosition.get().y - e.getPoint().y;
mainPanel.setSpacePosition(x, y);
mainPanel.repaint();
}
}
@Override
public void mouseMoved(final MouseEvent e) {
}
@Override
public void mouseEntered(final MouseEvent e) {
}
@Override
public void mouseExited(final MouseEvent e) {
}
@Override
public void mouseClicked(final MouseEvent e) {
}
}