Planet JFX
Advertisement

Introduction

Motivation / Goal

I have a need for a Windows XP Media Center-like table or list. I'm talking about the root-level table that comes up when Media center is first started. You can see Microsoft's own emulation of it from their product site.

Screenshot of XP MC in action

Note that this is only for the component itself, not the background or what happens when selection changes or when a row is clicked -- but we will need to support those things.

Requirements

If you have played around with Media Center or with the demo above, you will know that there is quite a lot of behavior to match. Let's try to enumerate what we expect out of this component:

  1. Input
    1. Support for keyboard -- a remote falls under this, since it is a safe assumption that a remote can be emulated as a keyboard. Up, down, select (enter or space, perhaps) are requirements, others may be nice to have.
    2. Support for mouse
      1. Mouse clicks -- you can click anything on the screen, even if it is not currently highlighted/selected. Note that while this click does change selection, it actually triggers an action on the clicked object as well.
      2. Mouseover -- some mouseover effects are present. This is not a hard requirement for me, so these will not be implemented.
  2. Display
    1. Limitless -- List items wrap seemlessly. User can continue indefinitely up or down. Note that there may be an additional spacer upon list wrap.
    2. Sliding -- smooth animated motions for the text. As up and down are pressed, the text slides under and through the highlight box.
    3. Row detail -- Rows generally consist of text, but we may want to allow more detail. The Highlighted row should certainly have an option to display more.
    4. Highlight/selection -- There is always a selected row. With keyboard motion, this highlighted box stays centered in the list, and the text scrolls through it. A mouse click elsewhere on the list will move the highlight box and start an action (see Action Feedback, below)
    5. Action feedback -- a small animation plays when an item in the list is acted upon, via mouse click or keyboard select.
    6. Fading items -- Central items are displayed in a crisp font and color. Items at the edge of the list slowly fade to transparent.
    7. Scrollable indicators -- Not on the MC list as displayed in the screenshot above, but having the option to turn on some kind of arrows indicating more content above and below would be nice. These arrows could also provide scrolling support when a keyboard is not available (like a Kiosk).
  3. Code Usability
    1. Reusable component -- self-contained in its own file
    2. Provides notifications of up/down/select actions
    3. Can interface with Java, perhaps via a PropertyChangeListener

Building the MediaTable Widget

The Basics

A very rough attempt

We have a long road ahead of us. To start out, I just decided to see if I can get a text array to show up.

MediaTable.fx
Code Preview
//SDK 1.0 ok
package mediacenter3;

import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.Scene;
import javafx.scene.text.Text;
import javafx.stage.Stage;

class MediaTableModel {
    var rows: String[];
    var currentRow: Number;
}

var model: MediaTableModel = MediaTableModel{
    rows: ["One","Two","Three", "Four", "Five", "Six"]
}

Stage {
    title: "Media Center 3"
    width: 150
    height: 300
    scene: Scene {
        content: Group{
            var rowY:Number;
            content:
            for (row in model.rows){
                Text{
                    stroke: Color.BLACK
                    content: row
                    y: ++rowY * 50
                    x: 50
                }
            }
        }
    }
}
Preview

Not too bad... I have a suspicion that rowNum will cause us some trouble if we are trying to dynamically add new rows, but it will do for now.

Some Cleanup

That code was simplistic, and not too usable. Let's start packaging the class for reuse.

MediaTable.fx
Code Preview
//SDK 1.0 ok
package mediacenter3;

import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.Scene;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;

class MediaTableModel {
    var rows: String[];
    var currentRow: Number;
}

class MediaTable extends CustomNode {
    package var model: MediaTableModel;
    package var width: Number;
    package var height: Number;
    public override function create(): Node {
        return
        Group {
            var rowNum = 0;
                content: //bind
                [
                    for (row in model.rows)
                    Text {
                        font: Font {
                            name: "sansserif"
                            size: 40
                            embolden: true
                        } // endfont
                        content: row
                        y: ++rowNum * 50
                        x: 2
                    } // endtext
                ]
        } // endgroup
        ;
    } // endoperation
}

var border = Rectangle {
    strokeWidth: 5
    stroke: Color.BLACK
    x: 25
    y: 25
    width: 300
    height: 200
    fill: null
};

var model: MediaTableModel = MediaTableModel{
    rows: ["One","Two","Three", "Four", "Five", "Six"]
}

Stage {
    title: "Media Center 3"
    width: 500
    height: 300
    scene: Scene {
        content: Group{
            content:[
                border,
                MediaTable{
                    model: model
                    transforms: [Transform.translate(25,25)]
                    width: bind border.width
                    height: bind border.height
                }
            ]
        }
    }
}
Preview

I have taken out some temporary code and values, and made MediaTable a full-fledged Node object that can be included easily in other Nodes.

Though it does nothing at this point, you can see that the intention is to constrain the MediaTable to a certain size -- inside the rectangle. It will then do what is needed at the edges (transparency fadeout, etc) and will hopefully respect the place it is given. The intention is that MediaTable will always work with components in its own 0,0 space. You then translate it when you use it, as shown here.

Clipping and the Start of something big

First off, let's fix clipping, so that the text is no longer scrolling off the bottom. Also, let's have a closer tie to the border, so I'll bind the translate to the border's x and y values. Lastly, let's do some ground work for calculating the position of the center row and showing a highlight.

MediaTable.fx
Code Preview
//SDK 1.O ok
package mediacenter3;

import javafx.lang.FX;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.Scene;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;
import javafx.scene.text.FontPosition;

class MediaTableModel {
    var rows: String[];
    var currentRow: Number;
}

class MediaTable extends CustomNode {
    package var model: MediaTableModel;
    package var width: Number;
    package var height: Number;
    package var rowHeight: Number; //this might move into Row object

    package function visibleRows():Number{
        return this.height / this.rowHeight;
    }

    package function centerRowTop():Number {
        return ( this.height - this.rowHeight) / 2;
    }

    public override function create(): Node {
        FX.println("visRows: {this.visibleRows()}");
        FX.println("centerY: {this.centerRowTop()}");

        return
        Group {
            var rowNum = 0;
            clip: Rectangle{
                x: 0
                y: 0
                height: this.height
                width: this.width
            }
            content: //bind
                [
                    for (row in this.model.rows)
                    Text {
                        font: Font {
                            name: "sansserif"
                            size: 40
                            embolden: true
                            position: FontPosition.SUPERSCRIPT
                        } // endfont
                        content: row
                        y: ++rowNum * 50
                        x: 2
                    } // endtext
                ,
                    Rectangle {
                        stroke: Color.BLUE
                        strokeWidth: 2
                        x: 0
                        y: bind centerRowTop()
                        height: bind rowHeight
                        width: bind width
                        fill:null
                    }
                ]
        } // endgroup
        ;
    } // endoperation
}

var border = Rectangle {
    strokeWidth: 5
    stroke: Color.BLACK
    x: 25
    y: 25
    width: 300
    height: 200
    fill: null
};

var model: MediaTableModel = MediaTableModel{
    rows: ["One","Two","Three", "Four", "Five", "Six"]
}

Stage {
    title: "Media Center 3"
    width: 500
    height: 300
    scene: Scene {
        content: Group{
            content:[
                border,
                MediaTable{
                    model: model
                    transforms: bind [Transform.translate(border.x,border.y)]
                    width: bind border.width
                    height: bind border.height
                    rowHeight: 50
                }
            ]
        }
    }
}
Preview

As you can see, we're not quite there, especially as the highlight doesn't seem to be centered as expected. But there are some interesting things. Functions (which appear to be the parts that aren't working), new attributes. I'm also beginning to wonder if MediaTable should take a Shape to clip on, and not width, height, and transform, done externally.

Calculations and scrolling

Let's fix the highlight row, add support for changing the center / selected row, and continute to tighten up the core MediaTable class.

MediaTable.fx
Code Preview
//SDK1.0 ok
package mediacenter3;

import javafx.lang.FX;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.input.MouseEvent;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.Scene;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.FontPosition;
import javafx.scene.text.Text;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;

class MediaTableModel {
    var rows: String[];
    var currentRow: Number;
}

class MediaTable extends CustomNode {
    package var model: MediaTableModel;
    package var centerRow: Number;
    package var rowHeight: Number; //this might move into Row object
    package var displayArea: Rectangle;
    package function visibleRows():Number{
        return displayArea.height / this.rowHeight;
    }

    package function centerRowTop():Number {
        return (
        displayArea.height - this.rowHeight) / 2;
    }

    package function rowOffset(row:Number):Number{
        return centerRowTop() + (row - centerRow) * rowHeight;
    }

    public override function create(): Node {
        FX.println("visRows: {this.visibleRows()}");
        FX.println("centerY: {this.centerRowTop()}");

        return
        Group {
            var rowNum = 0;
            transforms: bind [Transform.translate(displayArea.x,displayArea.y )]
            clip: Rectangle{
                x: 0
                y: 0
                height: displayArea.height
                width: displayArea.width
            }
                content: bind
                [
                    for (row in this.model.rows)
                    Text {
                        var indx  = indexof row
                        font: Font {
                            name: "sansserif"
                            size: 40
                            embolden: true
                            position: FontPosition.SUPERSCRIPT
                        } // endfont
                        content: row
                        
                        // TODO fix next line... need to get height of this text object, instead of 35.
                        y: bind rowOffset(indx) + ((rowHeight - 35) / 2)
                        x: 5
                    } // endtext
                ,
                    Rectangle {
                        stroke: Color.BLUE
                        strokeWidth: 2
                        x: 0
                        y: bind centerRowTop()
                        height: bind rowHeight
                        width: bind displayArea.width
                        fill: Color.CYAN
                        opacity: 0.8
                    }
                ]
        } // endgroup
        ;
    } // endoperation
}

var border = Rectangle {
    strokeWidth: 5
    stroke: Color.BLACK
    x: 25
    y: 25
    width: 300
    height: 200
    fill: null
};

var model: MediaTableModel = MediaTableModel{
    rows: ["One","Two","Three", "Four", "Five", "Six"]
}

var mt : MediaTable = MediaTable{
    model: bind model
    //transforms: bind [Transform.translate(border.x,border.y)]
    displayArea: bind border
    rowHeight: 50
}

Stage {
    title: "Media Center 3"
    width: 500
    height: 300
    scene: Scene {
        content: Group{
            content:[
                Rectangle{
                    x: 5
                    y: 23
                    width: 15
                    height: 15
                    fill: Color.RED
                    onMouseClicked: function (e:MouseEvent) {
                        FX.println("onMouseClick--:before{mt.centerRow}");
                        mt.centerRow--;
                        FX.println("onMouseClick--:after{mt.centerRow}");
                    }
                },
                Rectangle{
                    x: 5
                    y: 213
                    width: 15
                    height: 15
                    fill: Color.RED
                    onMouseClicked: function (e:MouseEvent) {
                        FX.println("onMouseClick++:before{mt.centerRow}");
                        mt.centerRow++;
                        FX.println("onMouseClick++:after{mt.centerRow}");
                    }
                },
                mt,
                border,
            ]
        }
    }
}
Preview

It turns out the highlight row problem was one of syntax; in function definitions, MediaTable.rowHeight and rowHeight are different. I suspect this is a static-versus-instance sort of thing.

I moved currentRow (which was unused before) from the table model into the table itself, since it really is a view feature. I also changed the attributes on the table to take a Rect instead of x, y, width, height, etc to facilitate easier usage.

For the GUI itself, I was able to get rid of the pesky rowNum variable -- turns out in a foreach you can simply do a indexof to get the index of the current loop variable. The highlight rectangle has changed a bit, and has a fill that lies under the text.

Finally, I've added two rectangles, that, when clicked, act as up and down operations.

Some next steps:

  • The up/down ops need to be internalized into the MediaTable itself, exposed as real operations.
  • Row sliding animation
  • Row wrapping

Some of these, wrapping in particular, may require some radical changes. In particular, we may have to divorce the rows variable from the row objects (Texts, right now) that are created. We may need two, three, or more copies of a row GUI object depending on the size of the data and its clipping area.

Wrapping

Woah, this is not so easy. There are still plenty of quirks and bugs in JavaFX, so you have to save often while using JavaFX Pad. I don't yet have sliding working -- I still have a feeling I need a more formalized View Model, or something.

MediaTable.fx
Code Preview
import javafx.ui.*;
import javafx.ui.canvas.*;
import javafx.ui.filter.*;

class MediaTableModel {
    attribute rows: String*;
}

class MediaTable extends CompositeNode {
    attribute model: MediaTableModel;
    attribute displayArea: Rect;
    attribute rowHeight: Integer; // this might move into a Row object...
    attribute centerRow: Integer;
    operation moveUp();
    operation moveDown();
    function visibleRows();
}

operation MediaTable.moveUp() {
    centerRow = normalizeRow(centerRow - 1);
}

operation MediaTable.moveDown() {
    centerRow = normalizeRow(centerRow + 1);
}

function MediaTable.visibleRows() {
    var visible:Integer = displayArea.currentHeight / rowHeight;
    return visible;
}

function MediaTable.bottomVisibleRow() {
    return centerRow + (visibleRows() / 2);
}

function MediaTable.topVisibleRow() {
    return centerRow - (visibleRows() / 2);
}

function MediaTable.centerRowTop() {
    return (displayArea.currentHeight - rowHeight) / 2;
}

function MediaTable.rowOffset(row) {
    return centerRowTop() + (row - centerRow) * rowHeight;
}

function MediaTable.normalizeRow(row) {
    var rowCount = sizeof model.rows;
    var rem:Integer = row % rowCount;
    return if (rem < 0) then rem + rowCount else rem;
}

operation MediaTable.composeNode() {
    return Clip {
        transform: bind translate(displayArea.currentX, displayArea.currentY)
        shape: Rect {
            x: 0
            y: 0
            width: bind displayArea.currentWidth
            height: bind displayArea.currentHeight
        }
        content: [
        Rect {
            stroke: navy
            fill: aqua
            strokeWidth: 2
            x: 0
            y: bind centerRowTop()
            height: bind rowHeight
            width: bind displayArea.currentWidth
        },
        Group {
            var topRow:Integer = bind topVisibleRow().intValue()
            var bottomRow:Integer = bind bottomVisibleRow().intValue() +1
            content:
            bind foreach (viewRow in [topRow..bottomRow])
            Text {
                var modelRow:Integer = normalizeRow(viewRow)
                font: Font {
                    face: SANSSERIF
                    size: 40
                    style: BOLD
                } // endfont
                content: model.rows[modelRow]
                // TODO fix next line... need to get height of this text object, instead of 35.
                y: bind rowOffset(viewRow) + (rowHeight - 35) / 2
                x: 5 // for fun: bind if (viewRow==centerRow) then [5..100] dur 2000 else [100..5] dur 1000
            } // endtext
            }, // endgroup
        ] // endcontent
        } // endclip
    ;
} // endoperation

// start demo code:
note: demo code left out; see previous example
Preview

So, there are now operations for moving up and down. Scrolling works pretty seamlessly, by essentially converting out-of-range rows back into in-range values. The normalizeRow() method has the logic for this.

Notice the intValue() calls when defining topRow and bottomRow. Since the top and bottom rows could be fractional, I was having a lot of trouble getting pure ints to iterate over for deciding which rows to draw. These calls helped me clear up the issue, and then the later foreach and array access were simple.

Making a component you want to use

Don't Fade Away

A lot of big changes coming in here. Keyboard support, though not arrow keys. More stratification into classes. More configurable parameters. The demonstration code is now formally split out into its own code.

MediaTable.fx
Code Preview
import javafx.ui.*;
import javafx.ui.canvas.*;
import javafx.ui.filter.*;
import java.lang.Math;

class MediaRowData {
	// TODO make a rowRenderer of some kind... pass in a class, have MediaTable create instances...
    attribute rowHeight: Integer;
    attribute rowFont: Font;
    attribute rowFill: Paint;
    attribute rowLeftMargin: Integer;
    attribute selectedRowBackground: Node;
} // endclass

class MediaTableModel {
    attribute rows: String*;
} // endclass

class MediaTable extends CompositeNode {
    attribute model: MediaTableModel;
    attribute rowData: MediaRowData;
    attribute displayArea: Rect;
    attribute centerRow: Integer;
    operation rowSelected(row);
    operation moveUp();
    operation moveDown();
    function visibleRows();
}

trigger on new MediaTable {
}

operation MediaTable.moveUp() {
    centerRow = normalizeRow(centerRow - 1);
}

operation MediaTable.moveDown() {
    centerRow = normalizeRow(centerRow + 1);
}

function MediaTable.visibleRows() {
    var visible:Integer = displayArea.currentHeight / rowData.rowHeight;
    return visible;
}

function MediaTable.bottomVisibleRow() {
    return centerRow + (visibleRows() / 2);
}

function MediaTable.topVisibleRow() {
    return centerRow - (visibleRows() / 2);
}

function MediaTable.centerRowTop() {
    return (displayArea.currentHeight - rowData.rowHeight) / 2;
}

function MediaTable.rowOffset(row) {
    return centerRowTop() + (row - centerRow) * rowData.rowHeight;
}

function MediaTable.normalizeRow(row) {
    var rowCount = sizeof model.rows;
    var rem:Integer = row % rowCount;
    return if (rem < 0) then rem + rowCount else rem;
}

operation MediaTable.rowSelected(row) {
    println("row clicked: {row}");
    centerRow = row;
}

operation MediaTable.composeNode() {	
    return Clip {
        transform: bind translate(displayArea.currentX, displayArea.currentY)
        shape: Rect {
            x: 0
            y: 0
            width: bind displayArea.currentWidth
            height: bind displayArea.currentHeight
        }
        content: [
	        rowData.selectedRowBackground,
	        Group {
	            var topRow:Integer = bind topVisibleRow().intValue()
	            var bottomRow:Integer = bind bottomVisibleRow().intValue() +1
	            content:
	            bind foreach (viewRow in [topRow..bottomRow])
	            Group {
	                // TODO fix next line... need to get height of this text object, instead of 35.
	                var rowTop = bind rowOffset(viewRow) + (rowData.rowHeight - 35) / 2
	                var modelRow:Integer = normalizeRow(viewRow)
	                content: [
		                Text {
			                opacity: bind 1 - (Math.abs(viewRow - centerRow) / visibleRows() * 1.5)
			                font: bind rowData.rowFont
			                fill: bind rowData.rowFill
			                content: model.rows[modelRow]
			                y: bind rowTop
			                x: rowData.rowLeftMargin // for fun: bind if (viewRow==centerRow) then [5..100] dur 2000 else [100..5] dur 1000
			            }, // endtext
		                Rect {
		                    x: 2
		                    y: bind rowTop - 7
		                    width: bind displayArea.currentWidth
		                    height: bind rowData.rowHeight
		                    stroke: black
		                    fill: black
		                    selectable: true
		                    opacity: .0001
		                    onMouseClicked: operation(e) {
		                        rowSelected(modelRow);
		                    } // endclick
	                    }, // endrect
		                ] // endcontent
	                } // endgroup
	            }, // endgroup
	        ] // endcontent
        } // endclip
    ;
} // endoperation

Mediatable-fading1

Preview (click to enlarge)

Here is the new test file:

TestMediaTable.fx
Code
import javafx.ui.*;
import javafx.ui.canvas.*;
import javafx.ui.filter.*;
import MediaTable;

// start demo code:

var border = Rect {
    strokeWidth: 5
    x: 25
    y: 25
    width: 300
    height: 200
    stroke: black
};

var mtModel = MediaTableModel {
    rows: ["Zero", "One", "Two", "Three", "Four", "Five", "Six"]
};

var rowHeight = 50;

var mt = MediaTable {
    model: bind mtModel
    displayArea: bind border
    rowData: MediaRowData {
	    rowHeight: rowHeight
	    rowFont: Font {
	        face: SANSSERIF
	        size: 40
	        style: BOLD
	    } // endfont
	    rowFill: green
	    selectedRowBackground: Rect {
	        stroke: gray
	        fill: lightGray
	        strokeWidth: 2
	        x: 0
	        y: bind (border.currentHeight - rowHeight) / 2 // TODO fix; copied from MT.cRT
	        height: bind rowHeight
	        width: bind border.currentWidth
		}
		rowLeftMargin: 50
    }
};

return Frame {
	title: "MediaTable Test Frame"
	content: 
	Canvas {
//	  	selectable: true
    	focusable: true
    	focused: true
    	onKeyTyped: operation(e:KeyEvent) {
    		println("canvas keyChar: '{e.keyChar}'");
			if (e.keyChar == "w") {
			    //println("W(Uwp) key pressed");
			   	mt.moveUp();
			} else if (e.keyChar == "s") {
			    //println("S(Down) key pressed");
			   	mt.moveDown();
			} // endif
    	} // end onKeyTyped
	
	    content: [
	    Rect {
	        x: 5
	        y: 23
	        width: 15
	        height: 15
	        fill: red
	        onMouseClicked: operation(e:CanvasMouseEvent) {
	            mt.moveUp();
	        }
	    },
	    Rect {
	        x: 5
	        y: 213
	        width: 15
	        height: 15
	        fill: red
	        onMouseClicked: operation(e:CanvasMouseEvent) {
	            mt.moveDown();
	        }
	    },
	    mt,
	    border,
	    ]
    } 
    visible: true
    width: 400
    height: 400
}; // endframe

As you can see, there are many more row rendering attributes that can be specified. Unfortunately, I could not really get sliding animation working yet. Row fading is in there, but I'm not quite happy with it... I'd like a better fade out than a per-line fade. On the plus side, it handles different table heights with no problems.

Keyboard is in there, barely, but only takes affect after a click on the red boxes... and since the arrow keys are not detected, I use w and s.

Checkpoint

Let's see how we are doing so far with our list of requirements:

  1. Input
    1. Support for keyboard -- Moderate. Arrow keys don't work, generally support is iffy
    2. Support for mouse
      1. x Mouse clicks -- Looking fine, working as expected
      2. Mouseover --
  2. Display
    1. x Limitless -- Working by "tricking" data model
    2. Sliding --
    3. Row detail -- Moderate. Additional parameters are available allowing customization, but we still may need a more formal row rendering ability.
    4. Highlight/selection -- Moderate. There is always a selection, but it does not move in response to an off-selected click; instead, the row clicked on becomes the center row.
    5. Action feedback --
    6. Fading items -- Moderate. Each line of text is at a different alpha level. There is not a smooth fade to background for the farthest rows.
    7. Scrollable indicators --
  3. Code Usability
    1. x Reusable component -- As we want, it is self-contained in its own file. It has several extra classes that control its behavior.
    2. x Provides notifications -- Mostly complete. Code can bind to the centerRow attribute for scrolling notifications, and can set the rowSelected operation to be notified of selection actions.
    3. Can interface with Java --

Down the slide

Let's fix this sliding thing. Run in combination with TestMediaTable, above.

MediaTable.fx
Code
import javafx.ui.*;
import javafx.ui.canvas.*;
import javafx.ui.filter.*;
import java.lang.Math;

class MediaRowData {
	// TODO make a rowRenderer of some kind... pass in a class, have MediaTable create instances...
    attribute rowHeight: Integer;
    attribute rowFont: Font;
    attribute rowFill: Paint;
    attribute rowLeftMargin: Integer;
    attribute selectedRowBackground: Node;
    attribute selected: function(row);
} // endclass

class MediaTableModel {
    attribute rows: String*;
} // endclass

class MediaTable extends CompositeNode {
    attribute model: MediaTableModel;
    attribute rowData: MediaRowData;
    attribute displayArea: Rect;
    attribute centerRow: Integer;
    private attribute animOffset: Integer; // used for anim
    private attribute animCounter: Integer; // used for anim
    private operation rowSelected(row);
    operation moveUp();
    operation moveDown();
    function visibleRows();
}

operation MediaTable.moveUp() {
	if (animOffset == 0) {
		animCounter = 0;
	    animOffset = [-rowData.rowHeight..0] dur 700 while animCounter == 0;

    } else {
    	// almost works:
    	var myCount = ++animCounter;
    	var newHeight = animOffset - rowData.rowHeight + 10;
	    animOffset = [newHeight..0] dur 450 linear while animCounter == myCount;
    } // endif

    centerRow = normalizeRow(centerRow - 1);
}

operation MediaTable.moveDown() {
	if (animOffset == 0) {
		animCounter = 0;
	    animOffset = [rowData.rowHeight..0] dur 700 while animCounter == 0;

    } else {
    	// almost works:
    	var myCount = ++animCounter;
    	var newHeight = animOffset + rowData.rowHeight - 20;
	    animOffset = [newHeight..0] dur 450 linear while animCounter == myCount;
    } // endif

    centerRow = normalizeRow(centerRow + 1);
}

function MediaTable.visibleRows() {
    var visible:Integer = displayArea.currentHeight / rowData.rowHeight;
    return visible;
}

function MediaTable.bottomVisibleRow() {
    return centerRow + (visibleRows() / 2);
}

function MediaTable.topVisibleRow() {
    return centerRow - (visibleRows() / 2);
}

function MediaTable.centerRowTop() {
    return (displayArea.currentHeight - rowData.rowHeight) / 2;
}

function MediaTable.rowOffset(row) {
    return centerRowTop() + animOffset + (row - centerRow) * rowData.rowHeight;
}

function MediaTable.normalizeRow(row) {
    var rowCount = sizeof model.rows;
    var rem:Integer = row % rowCount;
    return if (rem < 0) then rem + rowCount else rem;
}

operation MediaTable.rowSelected(row) {
    // notify rowData:
    (rowData.selected)(row);
    centerRow = row;
}

operation MediaTable.composeNode() {	
    return Clip {
        transform: bind translate(displayArea.currentX, displayArea.currentY)
        shape: Rect {
            x: 0
            y: 0
            width: bind displayArea.currentWidth
            height: bind displayArea.currentHeight
        }
        content: [
	        rowData.selectedRowBackground,
	        Group {
	            var topRow:Integer = bind topVisibleRow().intValue()
	            var bottomRow:Integer = bind bottomVisibleRow().intValue() +1
	            content:
	            bind foreach (viewRow in [topRow..bottomRow])
	            Group {
	                // TODO fix next line... need to get height of this text object, instead of 35.
	                var rowTop = bind rowOffset(viewRow) + (rowData.rowHeight - 35) / 2
	                var modelRow:Integer = normalizeRow(viewRow)
	                content: [
		                Text {
			                opacity: bind 1 - (Math.abs(viewRow - centerRow) / visibleRows() * 1.5)
			                font: bind rowData.rowFont
			                fill: bind rowData.rowFill
			                content: model.rows[modelRow]
			                y: bind rowTop
			                x: rowData.rowLeftMargin 
			            }, // endtext
		                Rect {
		                    x: 2
		                    y: bind rowTop - 7
		                    width: bind displayArea.currentWidth
		                    height: bind rowData.rowHeight
		                    stroke: black
		                    fill: black
		                    selectable: true
		                    opacity: .0001
		                    onMouseClicked: operation(e) {
		                        rowSelected(modelRow);
		                    } // endclick
	                    }, // endrect
		                ] // endcontent
	                } // endgroup
	            }, // endgroup
	        ] // endcontent
        } // endclip
    ;
} // endoperation

Major changes are to the class definition (adding new private attributes for animation), the moveUp(), moveDown() operations, and the rowOffset() function. Notice the syntax I use to interrupt animations in moveUp() and moveDown()... I increment a counter and tie the animation to that counter. If no animation is in progress and one of these operations is called, the counter starts over. This gives a reasonably smooth transition if the button is clicked multiple times rapidly.

The biggest annoyance is that, since centerRow is changing immediately, but the display of the row is still in motion, combined with the fading solution we are using means the row alpha coloration moves off center.

Summary

This article was developed for a very old version of JFX. It will not be completed.

Advertisement