MattHicks.com

Programming on the Edge

I Hate JavaFX; I Love JavaFX!

Published by Matt Hicks under , , , , , on Friday, February 27, 2009
I've posted a few places and gotten into more than a few rants publicly about how awful I think it is that Sun has been pushing JavaFX to the Java community. This has not been based on what JavaFX provides, because I think that's absolutely spectacular, but rather that they created a whole new language (JavaFX Script) that they force you into using rather than Java code itself. I've heard all the well reasoned arguments for why this is from the binding support to simplified UI design concepts, but as a professional ActionScript developer as well as a professional Java developer, I believe that using a language that stinks of ActionScript is a major step backwards for the Java community. However, like I said, JavaFX has done an absolutely wonderful job of adding to the UI power of the Java arsenal though, and I want to use it. Since I know that the JavaFX Script compiles into Java byte-code, I knew there had to be some level of access to the underlying API structure directly from Java, but nobody seems to know how or where to do it. After quite a bit of digging I stumbled upon these two pages:

http://blogs.sun.com/javafx/entry/how_to_use_javafx_in

http://forums.sun.com/thread.jspa?threadID=5354921&tstart=1

They looked pretty ugly to me, but put me on the track to actually using JavaFX directly in Java without scripting. I sort of combined those two together to come up with what I think is a little bit more concise example:


package test;

import java.awt.BorderLayout;

import javax.swing.JFrame;
import javax.swing.JPanel;

import com.sun.scenario.scenegraph.JSGPanel;
import com.sun.scenario.scenegraph.SGNode;

import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;

public class TestJFX {
public static void main(String[] args) throws Exception {
Group group = new Group(); {
}
Rectangle rect = new Rectangle(); {
rect.$width.setAsFloat(200.0f);
rect.$height.setAsFloat(200.0f);
rect.$fill.set(Color.$BLUE);
}
Text text = new Text(); {
text.$x.setAsFloat(20.0f);
text.$y.setAsFloat(20.0f);
text.$content.set("Greetings Earthling!");
text.$fill.set(Color.$WHITE);
}
group.$content.insert(rect);
group.$content.insert(text);
SGNode node = group.getSGGroup();
JSGPanel sgPanel = new JSGPanel();
sgPanel.setScene(node);

JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
panel.add(sgPanel, BorderLayout.CENTER);

JFrame frame = new JFrame("Test JavaFX");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(panel);
frame.setSize(800, 600);
frame.setVisible(true);
}
}


This isn't the prettiest or most efficient way to code, but it's a start. Next I decided that I'd like to reproduce the Clock example that JavaFX shows here:

http://java.sun.com/javafx/1/tutorials/build-javafx-nb-app/

This gave me a much more practical run through JavaFX from the programmatic perspective and allowed me to verify that it is in fact possible to write a complete application using Java making calls to JavaFX:


package test;

import java.awt.BorderLayout;
import java.io.IOException;
import java.util.Calendar;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;

import com.sun.javafx.functions.Function0;
import com.sun.javafx.runtime.location.FloatBindingExpression;
import com.sun.javafx.runtime.location.FloatVariable;
import com.sun.scenario.scenegraph.JSGPanel;
import com.sun.scenario.scenegraph.SGNode;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.lang.Duration;
import javafx.scene.Group;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.paint.Color;
import javafx.scene.shape.ArcTo;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;

/**
* @author Matt Hicks (matt@matthicks.com)
*/
public class Clock {
private float radius;
private float centerX;
private float centerY;

private Calendar calendar;
private int hours;
private int minutes;
private int seconds;

private FloatVariable hoursVariable;
private FloatVariable minutesVariable;
private FloatVariable secondsVariable;

public Clock() throws IOException {
// Initial setup
calendar = Calendar.getInstance();
radius = 77;
centerX = 144;
centerY = 144;

nextTick();

// Build JavaFX clock
Group group = new Group(); {
ImageView imageView = new ImageView(); {
Image image = new Image(); {
image.$bufferedImage.set(ImageIO.read(getClass().getClassLoader().getResource("resource/clock_background.png")));
}
imageView.$image.set(image);

group.$content.insert(imageView);
}

Group face = new Group(); {
Translate translate = new Translate(); {
translate.$x.setAsFloat(centerX);
translate.$y.setAsFloat(centerY);
}
face.$transforms.insert(translate);

// Every third hour
for (int i = 3; i <= 12; i += 3) {
Text text = new Text(); {
translate = new Translate(); {
translate.$x.setAsFloat(-5.0f);
translate.$y.setAsFloat(5.0f);
}
text.$transforms.insert(translate);

text.$font.set(Font.font("Arial", 16));

text.$x.setAsFloat(radius * ((i + 0) % 2 * (2 - i / 3)));
text.$y.setAsFloat(radius * ((i + 1) % 2 * (3 - i / 3)));
text.$content.set(String.valueOf(i));
}
face.$content.insert(text);
}

// 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 every third hour
}

Circle circle = new Circle(); {
Rotate rotate = new Rotate(); {
rotate.$angle.setAsFloat(30.0f * i);
}
circle.$transforms.insert(rotate);
circle.$centerX.setAsFloat(radius);
circle.$radius.setAsFloat(3.0f);
circle.$fill.set(Color.$BLACK);
}
face.$content.insert(circle);
}

// Center circles
Circle circle = new Circle(); {
circle.$radius.setAsFloat(5.0f);
circle.$fill.set(Color.$DARKRED);
}
face.$content.insert(circle);
circle = new Circle(); {
circle.$radius.setAsFloat(3.0f);
circle.$fill.set(Color.$RED);
}
face.$content.insert(circle);

// Second hand
Line line = new Line(); {
Rotate rotate = new Rotate(); {
FloatBindingExpression exp = new FloatBindingExpression() {
public float computeValue() {
return seconds * 6;
}
};
secondsVariable = FloatVariable.make(exp);
rotate.$angle.bind(false, secondsVariable);
}
line.$transforms.insert(rotate);

line.$endY.setAsFloat(-radius - 3.0f);
line.$strokeWidth.setAsFloat(2.0f);
line.$stroke.set(Color.$RED);
}
face.$content.insert(line);

// Hour hand
Path path = new Path(); {
Rotate rotate = new Rotate(); {
FloatBindingExpression exp = new FloatBindingExpression() {
public float computeValue() {
return (hours + minutes / 60) * 30 - 90;
}
};
hoursVariable = FloatVariable.make(exp);
rotate.$angle.bind(false, hoursVariable);
}
path.$transforms.insert(rotate);

path.$fill.set(Color.$BLACK);

MoveTo e1 = new MoveTo(); {
e1.$x.setAsFloat(4.0f);
e1.$y.setAsFloat(4.0f);
}
path.$elements.insert(e1);
ArcTo e2 = new ArcTo(); {
e2.$x.setAsFloat(4.0f);
e2.$y.setAsFloat(-4.0f);
e2.$radiusX.setAsFloat(1.0f);
e2.$radiusY.setAsFloat(1.0f);
}
path.$elements.insert(e2);
LineTo e3 = new LineTo(); {
e3.$x.setAsFloat(radius - 15.0f);
e3.$y.setAsFloat(0.0f);
}
path.$elements.insert(e3);
}
face.$content.insert(path);

// Minute hand
path = new Path(); {
Rotate rotate = new Rotate(); {
FloatBindingExpression exp = new FloatBindingExpression() {
public float computeValue() {
return minutes * 6 - 90;
}
};
minutesVariable = FloatVariable.make(exp);
rotate.$angle.bind(false, minutesVariable);
}
path.$transforms.insert(rotate);

path.$fill.set(Color.$BLACK);

MoveTo e1 = new MoveTo(); {
e1.$x.setAsFloat(4.0f);
e1.$y.setAsFloat(4.0f);
}
path.$elements.insert(e1);
ArcTo e2 = new ArcTo(); {
e2.$x.setAsFloat(4.0f);
e2.$y.setAsFloat(-4.0f);
e2.$radiusX.setAsFloat(1.0f);
e2.$radiusY.setAsFloat(1.0f);
}
path.$elements.insert(e2);
LineTo e3 = new LineTo(); {
e3.$x.setAsFloat(radius);
e3.$y.setAsFloat(0.0f);
}
path.$elements.insert(e3);
}
face.$content.insert(path);

group.$content.insert(face);
}
}

Timeline timeline = new Timeline(); {
timeline.$repeatCount.setAsFloat(Timeline.$INDEFINITE);

KeyFrame kf = new KeyFrame(); {
kf.$time.set(Duration.valueOf(1000.0f));
kf.$canSkip.set(true);
kf.$action.set(new Function0() {
public Void invoke() {
nextTick();
return null;
}
});
}
timeline.$keyFrames.insert(kf);
}

// Display in Swing
SGNode node = group.getSGGroup();
JSGPanel sgPanel = new JSGPanel();
sgPanel.setScene(node);

JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
panel.add(sgPanel, BorderLayout.CENTER);

JFrame frame = new JFrame("JavaFX Clock Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(panel);
frame.setSize(800, 600);
frame.setVisible(true);

timeline.play();
}

public void nextTick() {
calendar.setTimeInMillis(System.currentTimeMillis());
seconds = calendar.get(Calendar.SECOND);
minutes = calendar.get(Calendar.MINUTE);
hours = calendar.get(Calendar.HOUR_OF_DAY);

if (secondsVariable != null) {
secondsVariable.invalidate();
minutesVariable.invalidate();
hoursVariable.invalidate();
}
}

public static void main(String[] args) throws Exception {
new Clock();
}
}


Now, I did make some slight alterations as the original example uses java.util.Date calls that are deprecated, so I updated to using Calendar instead. Also, I had a little trouble with the binding functionality and had to make calls to invalidate in the nextTick() method. Other than that I stayed quite true to the example and the code works quite nicely.

This is the first step in what I'm ultimately planning as a wrapper around Swing and JavaFX to make more powerful UIs without using a crappy scripting language. :)

See this re-output as an Applet:
http://captiveimagination.com/download/clock/clock.html

Flex WebBrowser Component

Published by Matt Hicks under , , , , , on Wednesday, February 18, 2009
I've been working on an acceptable solution to display web pages within my Flex applications and as any of you know that have done any research on it will find, it's frustrating how lacking Flex is in support for this. The ideal solution would be for Flex to provide a fully functional component that renders web pages utilizing the native web browser. Alas, it would seem that it's far too complicated for them to leverage the browser they are already running in. Instead, their solution is to create components that support just enough HTML to be completely annoying since you can't get anything really useful to display well. Alright, so enough of that, like most of my posts, I try to mix rant and useful information, so lets get to the latter.

Okay, so the first thing that I discovered is that the only way to display web pages within my Flex content was to use IFrames. There are other possibilities, but after investigation with each they all come up with major flaws (primarily centered around lacking functionality) and IFrame support seems to be the best solution out of a bunch of bad solutions.

Yes, I'm sure if you've spent any time looking into this you'll already know that there are plenty of people that have already written IFrame support components in Flex and they work moderately well. However, there are some major pitfalls they have not resolved that I find unacceptable:
  1. Clipping - Because IFrames float above the Flash context they float above EVERYTHING. So not only do not get proper clipping of an Alert or anything else appearing above the component, you don't get proper clipping when it's inside of a container with scrollbars. This extremely limits the usefulness of the component and makes it almost useless in many sites
  2. Size and location updates. So many of these implementations of IFrame support do such an awful job of properly sizing and adjusting the frame as the Flex container is adjusted.
  3. Multiple components on the same page. In many cases I would like to have several IFrames being displayed on the page at one time and very few of the implementations have support for multiple being displayed at any time.
Yes, all of those are valid, but there are solutions available for all but #1. For a while I wondered if it was even possible to solve, but have since proven otherwise. :)

The first hurdle was figuring out a way to clip IFrame content. This was actually a lot easier than I expected it to be. I simply create a DIV, use absolute positioning, overflow to hidden, and add an iframe to it using relative positioning. From there it's just a simple matter of updating the location and size of the "visible area" for the DIV and adjusting the IFrame within it to clip the relevant areas. This does leave us without a perfect solution as we can obviously only show a single rectangular clipping area, but it's a lot better than no clipping at all.

Here's the code for the webbrowser.js file:
// WebBrowser specific code
function verifyCreated(id) {
var div = document.getElementById('div' + id);
if (div == null) {
div = document.createElement('div');
div.setAttribute('id', 'div' + id);
div.style.position = 'absolute';
div.style.visibility = 'hidden';
div.style.overflow = 'hidden';
var body = document.getElementsByTagName("body")[0];
body.appendChild(div);
}
var iframe = document.getElementById('iframe' + id);
if (iframe == null) {
iframe = document.createElement('iframe');
iframe.setAttribute('id', 'iframe' + id)
iframe.style.position = 'relative';
iframe.style.backgroundColor = 'white';
iframe.style.borderStyle = 'none';
iframe.setAttribute('frameborder', '0');
div.appendChild(iframe);
}
return iframe;
}

function updateBrowser(id, x, y, width, height, clipX, clipY, clipWidth, clipHeight) {
if ((width <= 0) || (height <= 0)) {
hideBrowser(id);
return;
}
var iframe = verifyCreated(id);
iframe.style.left = -clipX;
iframe.style.top = -clipY;
iframe.style.width = width;
iframe.style.height = height;
iframe.style.display = '';

var div = document.getElementById('div' + id);
div.style.left = x + clipX;
div.style.top = y + clipY;
div.style.width = clipWidth;
div.style.height = clipHeight;
div.style.visibility = 'visible';
div.style.display = '';
}

function loadURL(id, url) {
var iframe = verifyCreated(id);
iframe.src = url;
}

function hideBrowser(id) {
var div = document.getElementById('div' + id);
div.style.visibility = 'hidden';
div.style.display = 'none';
}
So that solves the dilema of clipping. The next hurdle, which I was quite surprised was a hurdle at all, was the of determining the visible bounds of a Flex component. You see, I wrote a WebBrowser Flex component that represents the IFrame within Flex and receives all the events for changes to size, location, visibility, etc. and then relays them to the IFrame it manages. However, Flex seems to have absolutely no support for finding the visibles bounds on a Component. So, as my solution to most things, I wrote my own:

        public static function getVisibleBounds(component:UIComponent):Rectangle {
var r:Rectangle = new Rectangle();

var step:Number = 50;

var best:Rectangle = new Rectangle();

var yOffset:Number = 0;

// Find largest bounding area
do {
nextBounds(component, yOffset, step, r);
yOffset += r.y + r.height + step;

if (r.width * r.height > best.width * best.height) {
best.x = r.x;
best.y = r.y;
best.width = r.width;
best.height = r.height;
}
} while (r.x != -1);

// Expand bounds broadly
expandBounds(component, best, step, step);

// Expand bounds narrowly
expandBounds(component, best, 1, step);

component.graphics.clear();
component.graphics.beginFill(0xffffff);
component.graphics.drawRect(best.x, best.y, best.width, best.height);
component.graphics.endFill();

return best;
}

private static function expandBounds(component:UIComponent, r:Rectangle, step:int, stepJump:int):void {
// Look up
while (validateHorizontal(component, r.x, r.y - step, r.width, stepJump)) {
r.y -= step;
r.height += step;
}

// Look down
while (validateHorizontal(component, r.x, r.y + r.height + step, r.width, stepJump)) {
r.height += step;
}

// Look left
while (validateVertical(component, r.x - step, r.y, r.height, stepJump)) {
r.x -= step;
r.width += step;
}

// Look right
while (validateVertical(component, r.x + r.width + step, r.y, r.height, stepJump)) {
r.width += step;
}
}

private static function validateHorizontal(component:UIComponent, x:int, y:int, width:int, step:int):Boolean {
for (var i:int = x; i <= width; i += step) {
if (!isUnderPoint(component, i, y)) {
return false;
}
}
if (isUnderPoint(component, x, y)) {
if (isUnderPoint(component, x + width, y)) {
return true;
}
}
return false;
}

private static function validateVertical(component:UIComponent, x:int, y:int, height:int, step:int):Boolean {
for (var i:int = y; i <= height; i += step) {
if (!isUnderPoint(component, x, i)) {
return false;
}
}
if (isUnderPoint(component, x, y)) {
if (isUnderPoint(component, x, y + height)) {
return true;
}
}
return false;
}

private static function nextBounds(component:UIComponent, yOffset:Number, step:int, r:Rectangle):Rectangle {
r.x = -1;
r.y = -1;
r.width = -1;
r.height = 0;

var p:Point = findFirstVisible(component, yOffset, step);
if (p != null) {
r.x = p.x;
r.y = p.y;
for (var y:int = p.y + step; y <= component.height; y += step) {
var currentWidth:Number = 0;
for (var x:int = p.x + step; x <= component.width; x += step) {
if (isUnderPoint(component, x, y)) {
currentWidth += step;
}
}
if (r.width == -1) {
r.width = currentWidth;
} else if (r.width > currentWidth) {
return r;
}
r.height += step;
}
}

return r;
}

private static function findFirstVisible(component:UIComponent, yOffset:Number, step:int):Point {
for (var y:int = yOffset; y <= component.height; y += step) {
for (var x:int = 0; x <= component.width; x += step) {
if (isUnderPoint(component, x, y)) {
return new Point(x, y);
}
}
}
return null;
}

private static var underPoint:Point = new Point();
public static function isUnderPoint(component:UIComponent, localX:Number, localY:Number):Boolean {
underPoint.x = localX + 1;
underPoint.y = localY;
underPoint = component.localToGlobal(underPoint);
var a:Array = Application.application.stage.getObjectsUnderPoint(underPoint);
for (var i:int = a.length - 1; i >= 0; i--) {
var c:UIComponent = getComponent(a[i]);
if (c != null) {
if (c.mouseEnabled) {
return c == component;
}
}
}
return false;
}

public static function getComponent(obj:DisplayObject):UIComponent {
while (obj != null) {
if (obj is UIComponent) {
return UIComponent(obj);
}
obj = obj.parent;
}
return null;
}


Yes, that's an awful lot of code, but it does work, and it works quite well. I find the largest bounding visible rectangular area for the Component and return a Rectangle representing it. I work in large steps (50 pixels at a time) to increase the performance and then make minor adjustments at the end to correct for the margin of precision loss.

The only thing still necessary to post is the WebBrowser Flex component:

    import flash.display.DisplayObjectContainer;
import flash.events.Event;
import flash.events.TimerEvent;
import flash.external.ExternalInterface;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.utils.Timer;

import mx.containers.Canvas;
import mx.core.Application;
import mx.core.UIComponent;
import mx.events.FlexEvent;
import mx.events.MoveEvent;
import mx.events.ResizeEvent;

public class WebBrowser extends Canvas {
private static var ID_GENERATOR:int = 0;

[Inspectable(defaultValue=null)]
private var pageUrl:String = null;
private var initted:Boolean = false;

private var changed:Boolean;
private var lastChanged:Number;
private var hidden:Boolean;

private var lastX:Number = -1;
private var lastY:Number = -1;
private var lastW:Number = -1;
private var lastH:Number = -1;

public function WebBrowser() {
super();
id = String(++ID_GENERATOR);
this.addEventListener(FlexEvent.CREATION_COMPLETE, onFlex, false, 0, true);

var timer:Timer = new Timer(15);
timer.addEventListener(TimerEvent.TIMER, update);
timer.start();

setStyle('borderThickness', 0);
clipContent = false;

changed = false;
hidden = true;

lastChanged = -1;
}

private function onEvent(event:Event):void {
if (event.target is UIComponent) {
if (event.target.owns(this)) {
if (event.type == 'remove') {
// Hide it
visible = false;
}
validateWindow();
}
}
}

private function onFlex(event:FlexEvent):void {
validateWindow();
if (pageUrl != null) ExternalInterface.call('loadURL', id, pageUrl);
systemManager.addEventListener(FlexEvent.HIDE, onEvent, true, 0, true);
systemManager.addEventListener(FlexEvent.SHOW, onEvent, true, 0, true);
systemManager.addEventListener(MoveEvent.MOVE, onEvent, true, 0, true);
systemManager.addEventListener(ResizeEvent.RESIZE, onEvent, true, 0, true);
systemManager.addEventListener(FlexEvent.REMOVE, onEvent, true, 0, true);
systemManager.addEventListener(FlexEvent.ADD, onEvent, true, 0, true);
initted = true;
}

private function onHide(event:FlexEvent):void {
ExternalInterface.call('hideBrowser', id);
}

private function isVisible():Boolean {
var obj:DisplayObjectContainer = this;
while (obj != Application.application) {
if (obj == null) {
// Removed from container
return false;
}
if (!obj.visible) {
return false;
}
obj = obj.parent;
}
return true;
}

public function validateWindow():void {
if (!isVisible()) {
onHide(null);
return;
}
changed = true;
}

private function update(evt:TimerEvent):void {
var time:Number = new Date().time;

var p:Point = new Point(0, 0);
p = localToGlobal(p);
if ((p.x == 0) && (p.y == 0)) {
return;
}
if ((lastX != p.x) || (lastY != p.y) || (lastW != width) || (lastH != height)) {
changed = true;
} else if (isVisible() == hidden) {
changed = true;
} else if ((lastChanged != -1) && (time - lastChanged > 500)) {
changed = true;
}

if (changed) {
changed = false;
lastChanged = time;

if (isVisible()) {
hidden = false;
lastX = p.x;
lastY = p.y;
lastW = width;
lastH = height;

var rect:Rectangle = FlexUtilities.getVisibleBounds(this);
ExternalInterface.call('updateBrowser', id, p.x, p.y, width, height, rect.x, rect.y, rect.width, rect.height);
} else {
hidden = true;
onHide(null);
}
}
}

public function set source(_pageUrl:String):void {
pageUrl = _pageUrl;
if (initted) {
ExternalInterface.call('loadURL', id, pageUrl);
}
}

[Bindable(event="changeUrl")]
public function get source():String {
return pageUrl;
}
}
Well, that does it. I hope you found this useful and proof that IFrame support can actually be done moderately well in Flex. The biggest problem I still have is that it can still be moderately slow, but that's an underlying flaw of the IFrame displaying in Flex and not something I can do anything about unfortunately.