28 novembro 2008

Playing with layout: a custom JavaFX layout manager

Hi!

Did you looked at javafx.scene.Group class? In a first look, Group is only a simple collection of nodes. But, looking it more detailed, you will see that it has an important feature: a method to create layout managers! And JavaFX has two built in layouts: javafx.scene.layout.HBox and javafx.scene.layout.VBox.

The first one is a simple layout manager that puts all node in an horizontal line, one after other. And the second do the same, but in a vertical line. Try to use then, as you use Group.

But I need, for my application, a different layout manager: I need to put a collection of objects inside a box. This box has a limited size, and this object need to be arranged according to box size. So, I need an automatic grid layout, that defines the numbers of columns according to objects widths.

Some day ago, I've created this layout manager object, and only now it works fine. It works in horizontal or vertical orientation, and determines the number of columns/lines by objects sizes. I called it as AutoGridLayout, and it's code was here:

/*
* AutoGridLayout.fx
*
* Created on 12/11/2008, 17:26:00
*/

package brunogrossi.javafx.components;
import javafx.scene.*;

/**
* @author Bruno Grossi
*/

public class Alignment {
private attribute name: String;

/**
* Represents the central position.
*/
public static attribute CENTER = Alignment { name: "CENTER" }

/**
* Represents the begin position: LEFT when horizontal, TOP when vertical
*/
public static attribute BEGIN = Alignment { name: "BEGIN" }

/**
* Represents the end position: RIGTH when horizontal, BOTTOM when vertical
*/
public static attribute END = Alignment { name: "END" }

public function toString(): String { name }
}

public class AutoGridLayout extends Group {
/**The max width or height of this layout component*/
public attribute maxSize:Number on replace {
impl_requestLayout();
}

/**Indicates if it's horizontal(true) or vertical(false) layout*/
public attribute horizontal:Boolean=true on replace {
impl_requestLayout();
}

/**The spacing between elements*/
public attribute spacing:Number on replace {
impl_requestLayout();
}

/**The spacing between elements*/
public attribute alignment:Alignment=Alignment.CENTER on replace {
impl_requestLayout();
}

init {
impl_layout = doLayout;
}

private function doLayout(g:Group):Void {
if (sizeof this.content>0 and this.maxSize>0) {
var largerSize:Number = 0.0;

var size:Number;
for (node in this.content) {
if (node.visible) {
size = if (horizontal) node.getBoundsWidth() else node.getBoundsHeight();
if (size > largerSize)
largerSize = size;
}
}
var numberOfElements:Integer =
java.lang.Math.floor(this.maxSize / largerSize) as Integer;
size = this.maxSize / numberOfElements;
var maxOtherSize:Number=0.0;

var x:Number = 0;
var y:Number = 0;

for (node in this.content) {
if (node.visible) {
node.impl_layoutX = if (horizontal)
calcPosition(x, size, node.getBoundsWidth())
else x;
node.impl_layoutY = if (not horizontal)
calcPosition(y, size, node.getBoundsHeight())
else y;

if (indexof node mod numberOfElements == numberOfElements-1) {
if (horizontal) {
x=0;
y+=maxOtherSize+spacing;
maxOtherSize=0;
} else {
y=0;
x+=maxOtherSize+spacing;
maxOtherSize=0;
}
} else {
var otherSize;
if (horizontal) {
x += size + spacing;
otherSize = node.getBoundsHeight();
} else {//Vertical
otherSize = node.getBoundsWidth();
y += size + spacing;
}
if (otherSize>maxOtherSize)
maxOtherSize = otherSize;
}

}
}
}
}

private function calcPosition(pos:Number, size:Number, nodeSize:Number):Number {
if (this.alignment==Alignment.CENTER) {
var _center = pos + (size/2);
_center - (nodeSize/2);
} else if (this.alignment==Alignment.END) {
var _end = pos + size;
_end - nodeSize;
} else {//BEGIN is default option
pos;
}
}

}


Here is a demonstration code that uses the AutoGridLayout. You can test it with box of randon size or with same size, only changing "randomSize" value...

//Demo
function randSize():Number {
java.lang.Math.random()*50+50;
}

import javafx.scene.paint.Color;
var f:javafx.application.Frame = javafx.application.Frame {
var randomSize = true;
var colors = [Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW, Color.BLACK]
visible:true
width:1000
height:1000
stage: javafx.application.Stage {
content: AutoGridLayout {
maxSize: 1000
horizontal:true
alignment: Alignment.CENTER
content:
for(i in [0..70])
javafx.scene.geometry.Rectangle{
width:if (randomSize) randSize() else 100
height:if (randomSize) randSize() else 100
fill:colors[i mod sizeof colors]
}
}
}
}


Here is an image of the demo, using random size boxes, in horizontal orientation. Notes that each line begins behind the other and each column has same size.

14 novembro 2008

Java/JavaFX integration: Implementing a Java Interface on JavaFX and Multiple Inheritance

One of most curious things of JavaFX is the possibility of multiple inheritance, a feature not present on it's "parent language" Java (take a look here). When the Java was created, the designers decided that multiple inheritance was a confusing and not optimized thing. So, to avoid future problems, they created Interfaces, an virtual collection of method signatures that must be implemented by an concrete class, but there're no self implementation.

Resuming, an Interface can extends various other Interfaces, and a Class can implement various Interfaces, but it can extend only one other class. When a Class implements an Interface, we say that it implements those methods specified on that Interface.

Back to JavaFX: JavaFX hasn't Interfaces, and permits multiple inheritance of classes. This is a valid code:
public class ClassA extends ClassB, ClassC {

}

And that classes can be JavaFX classes or Java classes..

But, if JavaFX doesn't have Interface, how can we implement a Java interface? It's simple: JavaFX consider Java interfaces as abstract classes with abstract methods. So, it's possible to do things like this:
public class MyJavaFXClass extends java.io.Serializable, java.io.InputStream, java.io.StringWriter {
...
}

Serializable and InputStream are java interfaces, and StringWriter is a concrete class.

It works very well, but there are some things that we need to note: some of Java features was not implemented in JavaFX yet, like varargs, enums and generics. Lets look each one:
  • Generics: is not a problem to inheritance. You can simple ignore then when implement a method. Java don't distinguishes between Collection and Collection in a method signature.

  • Enum: can be used on method signatures, but it's manipulated as a common object. You can't access enum values direct, like MyEnum.VALUE1. But you can use myEnumValue.name() method to compare string name of the values. Or you can user MyEnum.values() static method to get all values declared there.

  • Primitive types: It isn't a problem. JavaFX doesn't have primitive types, but if you have a method that receive a primitive type, you can use the relative object. Look for correspondences here.

  • VarArgs: it's the more problematic feature to JavaFX. You can't implement an interface on a concreate class that has methods with varargs! VarArgs aren't arrays! And there are no other form to substitute then. So, to resolve this problem, you can implement an abstract Java class that implements that methods and delegate then to other method, implemented by your JavaFX class. It's your unique solution now. But I think that VarArgs will be implemented in a near future in JavaFX, because it exists in a current-build's reflection class javafx.reflect.FXFunctionType (on 11/14/2008).

  • Arrays attribute: this is a very important problem. If I have a Java interface/class with an method that receipt an array of elements, I can't override that. Example:
Java interface:
public interface TesteI1 {

public void method1(String[] values);

}

JavaFX class:
public class TesteF1 extends TesteI1 {
public function method1(values: String[]):Void {

}
}
This is a wrong code. Why? Because String[] in JavaFX isn't an array, but is a Sequence! To show this, use this code:
var f = ["String1", "String2"];
java.lang.System.out.println("f: {f.getClass()}");

This will show you that f is a com.sun.javafx.runtime.sequence.ArraySequence. You can send it as a parameter to a method that receives an array, like java.util.Arrays.asList(f), but it isn't an array! I think that it's will be corrected in the future.

12 novembro 2008

Creating a Scroller Panel in JavaFX

Today I've created an Scroller Panel in JavaFX. It was a good challenge.

Here is the code:
package brunogrossi.javafx.components;

import javafx.scene.*;
import javafx.scene.image.*;
import javafx.scene.geometry.*;
import javafx.scene.paint.*;

/**
* @author Bruno Grossi
*/

private class Scroll extends CustomNode {
public attribute width:Integer;
public attribute height:Integer;
public attribute bodyHeight: Number;

private attribute position:Number;
private attribute barSizeMin:Number=30;
private attribute barSize:Number = 20;//bind if (height/(bodyHeight-height)< position="0"> maxPosition)
this.position=maxPosition
else
this.position=value;
}

postinit {
this.onMousePressed = function(e:javafx.input.MouseEvent):Void {
setPosition(e.getY() - (barSize / 2))
};

this.onMouseDragged = function(e:javafx.input.MouseEvent):Void {
setPosition(e.getY() - (barSize / 2))
};

this.onMouseWheelMoved = function(e:javafx.input.MouseEvent):Void {
setPosition(this.position + e.getWheelRotation())
};
}

protected function create():Node {
Group{
content: [
Rectangle {
width: bind width
height: bind height
fill: Color.WHITE
stroke: Color.WHITE
},
Line {
startX: bind width / 2
startY: bind 0
endX: bind width / 2
startY: bind height
stroke: Color.BLACK
strokeWidth: 1
effect: javafx.scene.effect.Shadow{radius:3}
},
Rectangle {
y: bind position
width: bind width
height: bind barSize
stroke: Color.BLACK
fill: Color.WHITESMOKE
arcWidth:10
arcHeight:20
smooth: true
effect: javafx.scene.effect.Lighting {
light: javafx.scene.effect.light.DistantLight{
azimuth: 60
elevation: 60
}
}
}
]
}

}
}

public class ScrollPanel extends CustomNode {
public attribute x:Integer;
public attribute y:Integer;
public attribute width:Integer;
public attribute height:Integer;

public attribute body: Node;
public attribute showScroll: Boolean = true;

private /*read-only*/ attribute scrollWidth = 15;
private /*read-only*/ attribute scroll:Scroll = Scroll{
translateX: bind width - scrollWidth - 1
width: scrollWidth
height: bind height
bodyHeight: bind body.getBoundsHeight()
};


postinit{
var onMouseWheelMovedOld = this.onMouseWheelMoved;
this.onMouseWheelMoved = function(e:javafx.input.MouseEvent):Void {
scroll.onMouseWheelMoved(e);
if (onMouseWheelMovedOld!=null)
onMouseWheelMovedOld(e);
};
}

protected function create():Node {
Group {
translateX: bind x
translateY: bind y
clip: Rectangle{width: bind width height: bind height}
content: [
Group{
clip: Rectangle{width: bind this.getInternalBoundsWidth() height: bind height}
content: Group{
content: body
translateY: bind -( scroll.position * (body.getBoundsHeight() - scroll.height)) / scroll.maxPosition
}
},
if (showScroll) scroll else null,
]
}
}

public function getInternalBoundsWidth(): Number {
if(showScroll) width - scrollWidth - 1 else width;
}
public function getInternalBoundsHeight(): Number {
height
}
}

/*This is for test */
var s = ScrollPanel {
width:100
height: 200
var colors = [Color.RED, Color.BLUE, Color.DARKGOLDENROD, Color.ALICEBLUE, Color.ANTIQUEWHITE, Color.AQUA, Color.AQUAMARINE, Color.AZURE, Color.BEIGE, Color.BISQUE, Color.CHOCOLATE]
body: Group{
content:
for(i in [0..30]) {[Rectangle{y: i * 10 width:99 height:10 fill:colors[
i mod sizeof colors] stroke:Color.BLACK},
javafx.scene.text.Text{y:i * 10 content:"{i}"}]
}
}
}

javafx.application.Frame {
visible: true
stage: javafx.application.Stage{
content: s
}
}