MattHicks.com

Programming on the Edge

JavaFX Wrapper: jSeamless 2.0

Published by Matt Hicks under on Saturday, March 07, 2009
I made a post recently (I Hate JavaFX; I Love JavaFX) in which I did a preliminary investigation on the idea of using the JavaFX API directly in Java rather than relying on JavaFX Script to do the job. I used the Clock example I saw on a tutorial on Sun's site as a practical example of something already done in JavaFX that showed several of the features of JavaFX. The resulting code was rather hideous as the API was not designed to be used directly, but it did work.

My next step was to begin writing a wrapper around this ugly Java API to give an elegant API structure for developing Java applications that utilize the awesome functionality of JavaFX for those people that don't care to use the scripting language. After a few days of work I'm close to having an alpha build ready to release and decided to redo the Clock example using this wrapper to see how easily it could be done.

I give you the resulting code:


package org.jseamless.example;

import java.util.Calendar;

import org.jseamless.Container;
import org.jseamless.View;
import org.jseamless.core.HorizontalAlignment;
import org.jseamless.core.VerticalAlignment;
import org.jseamless.effect.DropShadow;
import org.jseamless.paint.Color;
import org.jseamless.resource.URLResource;
import org.jseamless.shape.Ellipse;
import org.jseamless.shape.Line;
import org.jseamless.shape.path.ArcTo;
import org.jseamless.shape.path.LineTo;
import org.jseamless.shape.path.MoveTo;
import org.jseamless.shape.path.Path;
import org.jseamless.ui.Image;
import org.jseamless.ui.Text;
import org.xjava.bean.properties.Binding;
import org.xjava.bean.properties.Property;
import org.xjava.delegates.MethodDelegate;
import org.xjava.operation.Operation;
import org.xjava.operation.OperationParser;
import org.xjava.work.DelegateWorkUnit;
import org.xjava.work.ThreadManager;

public class Clock extends Container {
private float radius;
private float centerX;
private float centerY;

private Calendar calendar;
private Property hours;
private Property minutes;
private Property seconds;

public Clock() {
// Initial setup
calendar = Calendar.getInstance();
radius = 77.0f;
centerX = 144.0f;
centerY = 144.0f;

hours = new Property(null, 0.0f);
minutes = new Property(null, 0.0f);
seconds = new Property(null, 0.0f);

nextTick();

size.fill();

Image image = new Image(); {
image.source.set(new URLResource(getClass().getClassLoader().getResource("resource/clock_background.png")));

children.add(image);
}

Container face = new Container(); {
face.translate.x.set(centerX);
face.translate.y.set(centerY);

// Every third hour
for (int i = 3; i <= 12; i += 3) {
Text t = new Text(); {
t.translate.x.set(-5.0f);
t.translate.y.set(5.0f);
t.fontSize.set(16);
t.text.set(String.valueOf(i));

float x = radius * ((i + 0) % 2 * (2 - i / 3));
float y = radius * ((i + 1) % 2 * (3 - i / 3));
t.location.set(x, y);

face.children.add(t);
}
}

// Black circle for the rest of the hours
for (int i = 1; i < 12; i++) {
if (i % 3 == 0) {
continue; // Don't show a circle on the items that have text
}

Ellipse ellipse = new Ellipse(); {
ellipse.rotation.set(30.0f * i);
ellipse.location.x.set(radius);
ellipse.location.x.align.set(HorizontalAlignment.CENTER);
ellipse.location.y.align.set(VerticalAlignment.MIDDLE);
ellipse.size.set(6.0f, 6.0f);
ellipse.fill.set(Color.BLACK);

face.children.add(ellipse);
}
}

// Center circles
Ellipse ellipse = new Ellipse(); {
ellipse.location.x.align.set(HorizontalAlignment.CENTER);
ellipse.location.y.align.set(VerticalAlignment.MIDDLE);
ellipse.size.set(10.0f, 10.0f);
ellipse.fill.set(Color.DARK_RED);

face.children.add(ellipse);
}
ellipse = new Ellipse(); {
ellipse.location.x.align.set(HorizontalAlignment.CENTER);
ellipse.location.y.align.set(VerticalAlignment.MIDDLE);
ellipse.size.set(6.0f, 6.0f);
ellipse.fill.set(Color.RED);

face.children.add(ellipse);
}

// Second hand
Line line = new Line(); {
DropShadow ds = new DropShadow(); {
ds.offsetY.set(3.0f);

line.effect.set(ds);
}
Binding b = line.rotation.bind(seconds); {
OperationParser p = new OperationParser("(seconds * 6.0) + 90.0"); {
p.setVariable("seconds", seconds);
}
Operation operation = p.generateOperation();
b.setOperation(operation);
}

line.endX.set(-radius - 3.0f);
line.stroke.set(Color.RED);
line.stroke.width.set(2.0f);

face.children.add(line);
}

// Hour hand
Path path = new Path(); {
DropShadow ds = new DropShadow(); {
ds.offsetY.set(3.0f);

path.effect.set(ds);
}
Binding b = path.rotation.bind(seconds); {
OperationParser p = new OperationParser("((hours + (minutes / 60.0)) * 30.0) - 90.0"); {
p.setVariable("hours", hours);
p.setVariable("minutes", minutes);
}
Operation operation = p.generateOperation();
b.setOperation(operation);
}
path.fill.set(Color.BLACK);

MoveTo e1 = new MoveTo(); {
e1.x.set(4.0f);
e1.y.set(4.0f);

path.elements.add(e1);
}
ArcTo e2 = new ArcTo(); {
e2.x.set(4.0f);
e2.y.set(-4.0f);
e2.radiusX.set(1.0f);
e2.radiusY.set(1.0f);

path.elements.add(e2);
}
LineTo e3 = new LineTo(); {
e3.x.set(radius - 15.0f);
e3.y.set(0.0f);

path.elements.add(e3);
}

face.children.add(path);
}

// Minute hand
path = new Path(); {
DropShadow ds = new DropShadow(); {
ds.offsetY.set(3.0f);

path.effect.set(ds);
}
Binding b = path.rotation.bind(seconds); {
OperationParser p = new OperationParser("(minutes * 6.0) - 90.0"); {
p.setVariable("minutes", minutes);
}
Operation operation = p.generateOperation();
b.setOperation(operation);
}

path.fill.set(Color.BLACK);

MoveTo e1 = new MoveTo(); {
e1.x.set(4.0f);
e1.y.set(4.0f);

path.elements.add(e1);
}
ArcTo e2 = new ArcTo(); {
e2.x.set(4.0f);
e2.y.set(-4.0f);
e2.radiusX.set(1.0f);
e2.radiusY.set(1.0f);

path.elements.add(e2);
}
LineTo e3 = new LineTo(); {
e3.x.set(radius);
e3.y.set(0.0f);

path.elements.add(e3);
}

face.children.add(path);
}

children.add(face);
}
}

public void start() throws InterruptedException {
while (true) {
nextTick();

Thread.sleep(100);
}
}

public void nextTick() {
calendar.setTimeInMillis(System.currentTimeMillis());

seconds.set((float)calendar.get(Calendar.SECOND));
minutes.set((float)calendar.get(Calendar.MINUTE));
hours.set((float)calendar.get(Calendar.HOUR));
}

public static void main(String[] args) throws Exception {
Clock clock = new Clock();
View view = View.createWindow(clock);
view.title.set("jSeamless 2.0 Clock Example");
view.size.set("800px", "600px");

ThreadManager.getInstance().addWork(new DelegateWorkUnit(MethodDelegate.create(clock, "start")));
}
}


It looks strikingly similar to the uglier direct calls, but has been cleaned up a lot. This is still just the first pass, so more will come, but there are a ton of additional features and standardization features that have been thrown in as well as work-arounds for the broken key event functionality and so on.

I will be posting more in the near future as development continues on this wrapper, but I have decided this is the next logical step for jSeamless, so jSeamless 2.0 will not end up being a true UI abstraction, but rather a powerful wrapper around JavaFX and Swing functionality with some icing on the top.