/* 
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * Copyright 2009 Sun Microsystems, Inc. All rights reserved. Use is subject to license terms. 
 * 
 * This file is available and licensed under the following license:
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright notice, 
 *     this list of conditions and the following disclaimer.
 *
 *   * Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *
 *   * Neither the name of Sun Microsystems nor the names of its contributors 
 *     may be used to endorse or promote products derived from this software 
 *     without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package interesting;

import java.io.InputStream;
import java.lang.Exception;
import java.lang.Math;
import javafx.io.http.HttpRequest;
import javafx.animation.Timeline;
import javafx.animation.KeyFrame;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.scene.text.Text;
import javafx.scene.text.Font;
import javafx.scene.text.TextOrigin;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import interesting.model.Photo;
import interesting.parser.PhotoPullParser;
import interesting.view.FullImageView;
import interesting.view.ImageButton;
import interesting.view.ThumbImageView;
import interesting.view.ArrowImageButton;
import interesting.view.PageImageButton;
import interesting.view.ProgressBar;
import interesting.view.Layout;
import interesting.view.Background;

/**
 * Sample FX Script Application for viewing some of Flickr's Interesting Photos
 * Click on the photo to view larger photo.
 */

// TODO: get an apiKey from http://developer.yahoo.com/flickr
def apiKey = FX.getArgument("flickr_apikey");
var stageDragInitialX:Number;
var stageDragInitialY:Number;

// Application Width and Height
var dimension = "240X320";
var layout = Layout.getLayout(dimension);
var sceneWidth  : Number = bind scene.width;
var sceneHeight : Number = bind scene.height on replace {
    
    if(sceneHeight > 0) {
        
        fullImageView.show = false;
        
        try {
                        
            layout = Layout.getLayout("{sceneWidth as Integer}X{sceneHeight as Integer}");
            
            var lytW = Math.min(layout.width, layout.height);
            var lytH = Math.max(layout.width, layout.height);
            dimension = "{lytW}X{lytH}";
            
            updateThumbImageViews();
            updatePageButtons();
            updateImages();
            bgRect.requestFocus();
            
        } catch ( exception : Exception ) {
            alert("Error", "{exception}");
        }
    }
}

// Photo-Grid Page Index
var focusOnBack = false;
var focusOnNext = false;
public var focusOnGrid = false on replace {
    if(focusOnGrid) {
        focusOnBack = false;
        focusOnNext = false;
    }
};
var pageButtons: PageImageButton[]; // Page Buttons
var page:Integer = 0 on replace {
    for(pb in pageButtons) {
        pb.selected = false;
    }
}

// Information about all interesting photos
var photos: Photo[];

function getImage(imgURL : String, dimension : String) {
    Image { url: "{__DIR__}images/{dimension}/{imgURL}" };
}

// Application Background
var bgRect : Background = Background { 
    focusTraversable: true
    width: bind layout.width
    height: bind layout.height
    thumbBoundsX: bind layout.thumbGroupX
    thumbBoundsY: bind layout.thumbGroupY
    thumbBoundsW: bind layout.thumbGroupW
    thumbBoundsH: bind layout.thumbGroupH
    descTextY: bind layout.descTextY
    onKeyPressed:function(e:KeyEvent) {
        if(fullImageView.show) { 
            if(e.code == KeyCode.VK_ENTER) {
                fullImageView.show = false; 
            }
        } else if(e.code == KeyCode.VK_LEFT) {
            onLeftArrowKey();
        } else if(e.code == KeyCode.VK_RIGHT) {
            onRightArrowKey();
        } else if(e.code == KeyCode.VK_UP) {
            onUpArrowKey();
        } else if(e.code == KeyCode.VK_DOWN) {
            onDownArrowKey();
        } else if(e.code == KeyCode.VK_ENTER) {
            if(focusOnGrid) {
                showFullImage(thumbSelIndex);
            } else if(focusOnNext) {
                onNextClick();
            } else if(focusOnBack) {
                onBackClick();
            }
        }
    }
    onMousePressed:function(e:MouseEvent):Void {
        bgRect.requestFocus();
    }
}

function onLeftArrowKey() {
    if(focusOnGrid) {
        if(thumbSelIndex > 0) { 
            thumbSelIndex--; return;
        }
        onBackClick();
    } else {
        focusOnBack = true;
        focusOnNext = false;
    }
}

function onUpArrowKey() {
    
    if(not focusOnGrid) { 
        focusOnGrid = true; 
        thumbSelIndex = 0;
        return; 
    }
    
    if(thumbSelIndex < thumbCols) { // On first row
        focusOnGrid = false;
        focusOnBack = true;        
    } else { // On other rows
        thumbSelIndex = thumbSelIndex - thumbCols;
    }
}

function onDownArrowKey() {
    
    if(not focusOnGrid) { 
        focusOnGrid = true; 
        thumbSelIndex = 0;
        return; 
    }
    
    if(thumbSelIndex >= (thumbCols * (thumbRows - 1))) { // On last row
        focusOnGrid = false;
        focusOnNext = true;
    } else {
        thumbSelIndex = thumbSelIndex + thumbCols;
    }
}

function onRightArrowKey() {
    if(focusOnGrid) {
        if(thumbSelIndex < (sizeof thumbImageViews) - 1) { 
            thumbSelIndex++; return;
        }
        onNextClick();
    } else {
        focusOnBack = false;
        focusOnNext = true;
    }
}

// Dispose Application
var closeButton:ImageView = ImageButton { 
    
    x: bind (layout.width - closeButton.image.width - 10)
    y: 10
    normalImage: bind getImage("close_n.png", dimension)
    hoverImage: bind getImage("close_h.png", dimension)
    visible: bind ("{__PROFILE__}" != "browser")
    
    onMousePressed: function(e) {
        javafx.lang.FX.exit();
    }
}

// Display next set of photos
var nextButton : ArrowImageButton = ArrowImageButton { 
    
    x: bind pageButtonGroup.translateX + layout.pageGroupW - 7
    y: bind pageButtonGroup.translateY + layout.pageButtonH/2.0 - 19
    normalImage: bind getImage("next_n.png", dimension)
    hoverImage: bind getImage("next_h.png", dimension)
    active: bind focusOnNext
    visible: bind (not fullImageView.visible)
        
    onMousePressed: function(e) {
        nextButton.image = nextButton.hoverImage;
        onNextClick();
    }
}
function onNextClick() {
    
    if(sizeof photos <= 0) return;
    
    fullImageView.show = false;
    page++;
    if(page >= (sizeof pageButtons)) { page = 0; }
    thumbSelIndex = 0;
    updateImages();
}

// Display previous set of photos
var backButton : ArrowImageButton = ArrowImageButton { 
    
    x: bind pageButtonGroup.translateX - 33 + 7
    y: bind pageButtonGroup.translateY + layout.pageButtonH/2.0 - 38/2.0
    normalImage: bind getImage("back_n.png", dimension)
    hoverImage: bind getImage("back_h.png", dimension)
    active: bind focusOnBack
    visible: bind (not fullImageView.visible)
    
    onMousePressed: function(e) {
        backButton.image = backButton.hoverImage;
        onBackClick();
    }
}
function onBackClick() {
    
    if(sizeof photos <= 0) return;
    
    fullImageView.show = false;
    page--;
    if(page < 0) { page = (sizeof pageButtons) - 1; }
    thumbSelIndex = 8;
    updateImages();
}

public function setPage(index:Integer) {
    fullImageView.show = false;
    page = index;
    thumbSelIndex = 0;
    updateImages();
}

// Trim the string if length is greater than specified length
public function trimString(string:String, length:Integer) : String {
        
    if(string == null) return "";
    if(string.length() > length) { 
        return "{string.substring(0, length).trim()}..."; 
    } else {
        return string;
    }
}

// Photo Description and Page details
var descText: Text;
public var description = "Loading Photos...";

var thumbCols = bind layout.thumbCols;
var thumbRows = bind layout.thumbRows;

// Initialize Photo-Grid
var thumbImageViews: ThumbImageView[]; // Thumbnail images
public var thumbSelIndex = 0 on replace {
    for(tiv in thumbImageViews) {
        tiv.selected = false;
    }
    thumbImageViews[thumbSelIndex].selected = true;
}
var thumbImageViewGroup : VBox = VBox { 
    translateX: bind (layout.width - layout.thumbGroupW)/2.0
    translateY: bind (layout.height - layout.thumbGroupH)/2.0
    spacing: 0
};
function updateThumbImageViews() {
        
    // reset    
    delete thumbImageViews;
    delete thumbImageViewGroup.content;
    
    for(row in [0..(thumbRows - 1)]) {
        var hBox = HBox { spacing: 0 };
        for(col in [0..(thumbCols - 1)]) {
            def thumbImageView = ThumbImageView { 
                width: layout.thumbSize
                height: layout.thumbSize
                index: (sizeof thumbImageViews)
            }
            insert thumbImageView into thumbImageViews;
            insert thumbImageView into hBox.content;
        }
        insert hBox into thumbImageViewGroup.content;
    }
    
    if(thumbSelIndex >= (sizeof thumbImageViews)) { thumbSelIndex = 0; }
    thumbImageViews[thumbSelIndex].selected = true;
}

var pageButtonImage = bind getImage("tab_n.png", dimension);
var pageButtonHoverImage = bind getImage("tab_h.png", dimension);
var pageButtonSelectImage = bind getImage("tab_s.png", dimension);

function updatePageButtons() {
    
    delete pageButtons;
    
    for(row in [0..layout.pageCount]) {
        insert PageImageButton {
            normalImage: pageButtonImage
            hoverImage: pageButtonHoverImage
            selectImage: pageButtonSelectImage
            index: (sizeof pageButtons)
        } into pageButtons;
    }

    if(page >= (sizeof pageButtons)) { page = 0; }
}

function getPageButtonY(y : Number) : Number {
    var maxY = y + layout.thumbGroupH;
    var maxH = layout.height - maxY;
    return maxY + maxH/2.0 - layout.pageButtonH/3.0;
}
var pageButtonGroup : HBox = HBox {
    content: bind pageButtons
    spacing: -7
    translateX: bind (layout.width - layout.pageGroupW)/2.0
    translateY: bind getPageButtonY(layout.thumbGroupY);
}

// Initialize fullscreen ImageView
public var fullImageView = FullImageView {
    x: bind layout.thumbGroupX
    y: bind layout.thumbGroupY
    width: bind layout.thumbGroupW
    height: bind layout.thumbGroupH
    visible: false
}
var progressBar : ProgressBar = ProgressBar {
    translateX: bind (layout.thumbGroupX + 10)
    translateY: bind (layout.thumbGroupY + layout.thumbGroupH - 6)
    width: bind (layout.thumbGroupW - 20)
    visible: bind progressTimeline.running
}
var progressTimeline:Timeline = Timeline {
    repeatCount: Timeline.INDEFINITE
    keyFrames: [ 
        KeyFrame {
            time: 100ms
            action: function() {
                progressBar.progress = getProgress();
                if(fullImageView.visible) {
                    progressBar.opacity = fullImageView.opacity;
                } else {
                    progressBar.opacity = 1.0;
                }
                if(progressBar.progress == 100.0) {
                    progressTimeline.stop();
                }
            }
        }
    ]
};

function getProgress() : Number {
    if(fullImageView.visible) {
        return fullImageView.image.progress;
    } else {
        var totalProgress = 0.0;
        for(i in [0..((sizeof thumbImageViews) - 1)]) {
            totalProgress = totalProgress + thumbImageViews[i].image.progress;
        }
        if(totalProgress == 0) { return 0; }
        return totalProgress/(sizeof thumbImageViews); 
    }
}

public function showFullImage(index:Integer) {
    
    var tiv = thumbImageViews[index];
    fullImageView.image = Image {
        url: "http://farm{tiv.photo.farm}.static.flickr.com/{tiv.photo.server}/{tiv.photo.id}_{tiv.photo.secret}_m.jpg"
        placeholder: tiv.image
        backgroundLoading: true
    };
    
    fullImageView.show = true;
    progressBar.progress = 0.0;
    progressTimeline.playFromStart();
}

// Load image and data specified in given Photo object
function loadImage(photo: Photo, thumbImageView: ThumbImageView): Void { 
    thumbImageView.image = Image {
        url: "http://farm{photo.farm}.static.flickr.com/{photo.server}/{photo.id}_{photo.secret}_s.jpg";
        backgroundLoading: true
        placeholder: thumbImageView.image
    };
    thumbImageView.photo = photo;
}

// Update images displayed in Photo-Grid
function updateImages() {
    
    if(not loadComplete) { return; }
    
    progressBar.progress = 0.0;
    progressTimeline.playFromStart();
    
    for(i in [0..(sizeof thumbImageViews) - 1]) {
        var photoIndex = (page * (sizeof thumbImageViews)) + i;
        loadImage(photos[photoIndex], thumbImageViews[i]);
    }
    
    description = trimString("{thumbImageViews[thumbSelIndex].photo.title}", 55);
    pageButtons[page].selected = true;
}

// Application Title-Bar
var titleBar = Rectangle {
    width: bind layout.width
    height: bind layout.thumbGroupY
    fill: Color.TRANSPARENT
    visible: bind ("{__PROFILE__}" != "browser")
     onMousePressed: function(e) {
        stageDragInitialX = e.screenX - stage.x;
        stageDragInitialY = e.screenY - stage.y;
    }
     onMouseDragged: function(e) {
        stage.x = e.screenX - stageDragInitialX;
        stage.y = e.screenY - stageDragInitialY;
     }

}

// Set stage content based on status of api-key
var stageContent: Node[];

function initErrorUI() {
    
    descText = Text {
        x: bind layout.thumbGroupX + 10
        y: bind layout.thumbGroupY + 10
        font: bind layout.descFont
        wrappingWidth: bind (layout.thumbGroupW - 20)
        content: "This application requires an api key from Flickr.\nGet yours from \nhttp://developer.yahoo.com/flickr"
        fill: Color.WHITE
        smooth: true
        textOrigin: TextOrigin.TOP
    }
    
    stageContent = [
        bgRect, titleBar, closeButton, descText
    ];
}

function initDefaultUI() {
    
    descText = Text {
        x: bind layout.thumbGroupX
        y: bind layout.descTextY
        font: bind layout.descFont
        wrappingWidth: bind (layout.thumbGroupW - 10)
        content: bind description
        fill: Color.WHITE
        smooth: true
        textOrigin: TextOrigin.TOP
        clip: Rectangle {
            x: bind descText.x
            y: bind (descText.y - 2)
            width: bind (layout.width - 20)
            height: bind ( layout.thumbGroupY - descText.y - 17)
        }
    }
    
    stageContent = [
        bgRect, titleBar, nextButton, backButton, closeButton,
        descText, thumbImageViewGroup, pageButtonGroup, 
        fullImageView, progressBar
    ];

    loadImageMetadata();    
}

var loadComplete = false;
function loadImageMetadata() {
    
    println("Loading image metadata...");   
    description = "Loading Photos...";
    
    var httpRequestError: Boolean = false;
    
    // Submit HttpRequest    
    var request: HttpRequest = HttpRequest {
        
        location: "http://api.flickr.com/services/rest/?method="
            "flickr.interestingness.getList&api_key={apiKey}&per_page={layout.imageCount}"
        method: HttpRequest.GET
       
        onRead: function(bytes: Long) {
            // The toread variable is non negative only if the server provides the content length
            def loadProgress = if (request.toread > 0) "{(bytes * 100 / request.toread)}%" else "";
            description = "Loading Photos... ({loadProgress})";
        }
        
        onException: function(exception: Exception) {
            exception.printStackTrace();
            alert("Error", "{exception}");
            httpRequestError = true;
        }
            
        onResponseCode: function(responseCode:Integer) {
            if (responseCode != 200) {
                println("failed, response: {responseCode} {request.responseMessage}");
            }
        }
                        
        onInput: function(input: java.io.InputStream) {
            
            try {
                
                var parser = PhotoPullParser { 
                    onDone: function( data:Photo[] ) { 
                        photos = data; 
                        if(not httpRequestError) {
                            loadComplete = true;
                            updateImages(); 
                            focusOnGrid = true;
                        }
                    } 
                };
                parser.parse(input);
                if(parser.errorMessage.length() > 0) { 
                    alert("Error", parser.errorMessage);
                    httpRequestError = true; 
                }
                
            } finally {
                input.close();
            }
        }
    }
    
    request.start();
}

function alert(alertTitle:String, msg:String): Void {
    println(msg);
    description = trimString("{alertTitle}: {msg}", 55);
}

function initUI() {    
    // If API-Key is empty, display error
    if (apiKey == null) {
        initErrorUI();
    } else { // Retrieve photo information and display
        initDefaultUI();    
    }
}

var scene : Scene = Scene {
    width: 240
    height: 320
    content: Group {
        content: bind stageContent
        clip: Rectangle {
            width: bind layout.width
            height: bind layout.height
            arcWidth: 20
            arcHeight: 20
        }
    }
    fill: Color.TRANSPARENT
};

// Application User Interface
var stage = Stage {
    title: "Interesting Photos"
    resizable: false
    width: 240
    height: 320
    visible: false
    style: StageStyle.TRANSPARENT
    scene: bind scene
}

function run() {
    initUI();    
    stage.visible = true;
    // Set focus to background by default
    bgRect.requestFocus();
}
JavaFX Sample

/* 
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * Copyright 2009 Sun Microsystems, Inc. All rights reserved. Use is subject to license terms. 
 * 
 * This file is available and licensed under the following license:
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright notice, 
 *     this list of conditions and the following disclaimer.
 *
 *   * Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *
 *   * Neither the name of Sun Microsystems nor the names of its contributors 
 *     may be used to endorse or promote products derived from this software 
 *     without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package interesting;

import java.io.InputStream;
import java.lang.Exception;
import java.lang.Math;
import javafx.io.http.HttpRequest;
import javafx.animation.Timeline;
import javafx.animation.KeyFrame;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.scene.text.Text;
import javafx.scene.text.Font;
import javafx.scene.text.TextOrigin;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import interesting.model.Photo;
import interesting.parser.PhotoPullParser;
import interesting.view.FullImageView;
import interesting.view.ImageButton;
import interesting.view.ThumbImageView;
import interesting.view.ArrowImageButton;
import interesting.view.PageImageButton;
import interesting.view.ProgressBar;
import interesting.view.Layout;
import interesting.view.Background;

/**
 * Sample FX Script Application for viewing some of Flickr's Interesting Photos
 * Click on the photo to view larger photo.
 */

// TODO: get an apiKey from http://developer.yahoo.com/flickr
def apiKey = FX.getArgument("flickr_apikey");
var stageDragInitialX:Number;
var stageDragInitialY:Number;

// Application Width and Height
var dimension = "240X320";
var layout = Layout.getLayout(dimension);
var sceneWidth  : Number = bind scene.width;
var sceneHeight : Number = bind scene.height on replace {
    
    if(sceneHeight > 0) {
        
        fullImageView.show = false;
        
        try {
                        
            layout = Layout.getLayout("{sceneWidth as Integer}X{sceneHeight as Integer}");
            
            var lytW = Math.min(layout.width, layout.height);
            var lytH = Math.max(layout.width, layout.height);
            dimension = "{lytW}X{lytH}";
            
            updateThumbImageViews();
            updatePageButtons();
            updateImages();
            bgRect.requestFocus();
            
        } catch ( exception : Exception ) {
            alert("Error", "{exception}");
        }
    }
}

// Photo-Grid Page Index
var focusOnBack = false;
var focusOnNext = false;
public var focusOnGrid = false on replace {
    if(focusOnGrid) {
        focusOnBack = false;
        focusOnNext = false;
    }
};
var pageButtons: PageImageButton[]; // Page Buttons
var page:Integer = 0 on replace {
    for(pb in pageButtons) {
        pb.selected = false;
    }
}

// Information about all interesting photos
var photos: Photo[];

function getImage(imgURL : String, dimension : String) {
    Image { url: "{__DIR__}images/{dimension}/{imgURL}" };
}

// Application Background
var bgRect : Background = Background { 
    focusTraversable: true
    width: bind layout.width
    height: bind layout.height
    thumbBoundsX: bind layout.thumbGroupX
    thumbBoundsY: bind layout.thumbGroupY
    thumbBoundsW: bind layout.thumbGroupW
    thumbBoundsH: bind layout.thumbGroupH
    descTextY: bind layout.descTextY
    onKeyPressed:function(e:KeyEvent) {
        if(fullImageView.show) { 
            if(e.code == KeyCode.VK_ENTER) {
                fullImageView.show = false; 
            }
        } else if(e.code == KeyCode.VK_LEFT) {
            onLeftArrowKey();
        } else if(e.code == KeyCode.VK_RIGHT) {
            onRightArrowKey();
        } else if(e.code == KeyCode.VK_UP) {
            onUpArrowKey();
        } else if(e.code == KeyCode.VK_DOWN) {
            onDownArrowKey();
        } else if(e.code == KeyCode.VK_ENTER) {
            if(focusOnGrid) {
                showFullImage(thumbSelIndex);
            } else if(focusOnNext) {
                onNextClick();
            } else if(focusOnBack) {
                onBackClick();
            }
        }
    }
    onMousePressed:function(e:MouseEvent):Void {
        bgRect.requestFocus();
    }
}

function onLeftArrowKey() {
    if(focusOnGrid) {
        if(thumbSelIndex > 0) { 
            thumbSelIndex--; return;
        }
        onBackClick();
    } else {
        focusOnBack = true;
        focusOnNext = false;
    }
}

function onUpArrowKey() {
    
    if(not focusOnGrid) { 
        focusOnGrid = true; 
        thumbSelIndex = 0;
        return; 
    }
    
    if(thumbSelIndex < thumbCols) { // On first row
        focusOnGrid = false;
        focusOnBack = true;        
    } else { // On other rows
        thumbSelIndex = thumbSelIndex - thumbCols;
    }
}

function onDownArrowKey() {
    
    if(not focusOnGrid) { 
        focusOnGrid = true; 
        thumbSelIndex = 0;
        return; 
    }
    
    if(thumbSelIndex >= (thumbCols * (thumbRows - 1))) { // On last row
        focusOnGrid = false;
        focusOnNext = true;
    } else {
        thumbSelIndex = thumbSelIndex + thumbCols;
    }
}

function onRightArrowKey() {
    if(focusOnGrid) {
        if(thumbSelIndex < (sizeof thumbImageViews) - 1) { 
            thumbSelIndex++; return;
        }
        onNextClick();
    } else {
        focusOnBack = false;
        focusOnNext = true;
    }
}

// Dispose Application
var closeButton:ImageView = ImageButton { 
    
    x: bind (layout.width - closeButton.image.width - 10)
    y: 10
    normalImage: bind getImage("close_n.png", dimension)
    hoverImage: bind getImage("close_h.png", dimension)
    visible: bind ("{__PROFILE__}" != "browser")
    
    onMousePressed: function(e) {
        javafx.lang.FX.exit();
    }
}

// Display next set of photos
var nextButton : ArrowImageButton = ArrowImageButton { 
    
    x: bind pageButtonGroup.translateX + layout.pageGroupW - 7
    y: bind pageButtonGroup.translateY + layout.pageButtonH/2.0 - 19
    normalImage: bind getImage("next_n.png", dimension)
    hoverImage: bind getImage("next_h.png", dimension)
    active: bind focusOnNext
    visible: bind (not fullImageView.visible)
        
    onMousePressed: function(e) {
        nextButton.image = nextButton.hoverImage;
        onNextClick();
    }
}
function onNextClick() {
    
    if(sizeof photos <= 0) return;
    
    fullImageView.show = false;
    page++;
    if(page >= (sizeof pageButtons)) { page = 0; }
    thumbSelIndex = 0;
    updateImages();
}

// Display previous set of photos
var backButton : ArrowImageButton = ArrowImageButton { 
    
    x: bind pageButtonGroup.translateX - 33 + 7
    y: bind pageButtonGroup.translateY + layout.pageButtonH/2.0 - 38/2.0
    normalImage: bind getImage("back_n.png", dimension)
    hoverImage: bind getImage("back_h.png", dimension)
    active: bind focusOnBack
    visible: bind (not fullImageView.visible)
    
    onMousePressed: function(e) {
        backButton.image = backButton.hoverImage;
        onBackClick();
    }
}
function onBackClick() {
    
    if(sizeof photos <= 0) return;
    
    fullImageView.show = false;
    page--;
    if(page < 0) { page = (sizeof pageButtons) - 1; }
    thumbSelIndex = 8;
    updateImages();
}

public function setPage(index:Integer) {
    fullImageView.show = false;
    page = index;
    thumbSelIndex = 0;
    updateImages();
}

// Trim the string if length is greater than specified length
public function trimString(string:String, length:Integer) : String {
        
    if(string == null) return "";
    if(string.length() > length) { 
        return "{string.substring(0, length).trim()}..."; 
    } else {
        return string;
    }
}

// Photo Description and Page details
var descText: Text;
public var description = "Loading Photos...";

var thumbCols = bind layout.thumbCols;
var thumbRows = bind layout.thumbRows;

// Initialize Photo-Grid
var thumbImageViews: ThumbImageView[]; // Thumbnail images
public var thumbSelIndex = 0 on replace {
    for(tiv in thumbImageViews) {
        tiv.selected = false;
    }
    thumbImageViews[thumbSelIndex].selected = true;
}
var thumbImageViewGroup : VBox = VBox { 
    translateX: bind (layout.width - layout.thumbGroupW)/2.0
    translateY: bind (layout.height - layout.thumbGroupH)/2.0
    spacing: 0
};
function updateThumbImageViews() {
        
    // reset    
    delete thumbImageViews;
    delete thumbImageViewGroup.content;
    
    for(row in [0..(thumbRows - 1)]) {
        var hBox = HBox { spacing: 0 };
        for(col in [0..(thumbCols - 1)]) {
            def thumbImageView = ThumbImageView { 
                width: layout.thumbSize
                height: layout.thumbSize
                index: (sizeof thumbImageViews)
            }
            insert thumbImageView into thumbImageViews;
            insert thumbImageView into hBox.content;
        }
        insert hBox into thumbImageViewGroup.content;
    }
    
    if(thumbSelIndex >= (sizeof thumbImageViews)) { thumbSelIndex = 0; }
    thumbImageViews[thumbSelIndex].selected = true;
}

var pageButtonImage = bind getImage("tab_n.png", dimension);
var pageButtonHoverImage = bind getImage("tab_h.png", dimension);
var pageButtonSelectImage = bind getImage("tab_s.png", dimension);

function updatePageButtons() {
    
    delete pageButtons;
    
    for(row in [0..layout.pageCount]) {
        insert PageImageButton {
            normalImage: pageButtonImage
            hoverImage: pageButtonHoverImage
            selectImage: pageButtonSelectImage
            index: (sizeof pageButtons)
        } into pageButtons;
    }

    if(page >= (sizeof pageButtons)) { page = 0; }
}

function getPageButtonY(y : Number) : Number {
    var maxY = y + layout.thumbGroupH;
    var maxH = layout.height - maxY;
    return maxY + maxH/2.0 - layout.pageButtonH/3.0;
}
var pageButtonGroup : HBox = HBox {
    content: bind pageButtons
    spacing: -7
    translateX: bind (layout.width - layout.pageGroupW)/2.0
    translateY: bind getPageButtonY(layout.thumbGroupY);
}

// Initialize fullscreen ImageView
public var fullImageView = FullImageView {
    x: bind layout.thumbGroupX
    y: bind layout.thumbGroupY
    width: bind layout.thumbGroupW
    height: bind layout.thumbGroupH
    visible: false
}
var progressBar : ProgressBar = ProgressBar {
    translateX: bind (layout.thumbGroupX + 10)
    translateY: bind (layout.thumbGroupY + layout.thumbGroupH - 6)
    width: bind (layout.thumbGroupW - 20)
    visible: bind progressTimeline.running
}
var progressTimeline:Timeline = Timeline {
    repeatCount: Timeline.INDEFINITE
    keyFrames: [ 
        KeyFrame {
            time: 100ms
            action: function() {
                progressBar.progress = getProgress();
                if(fullImageView.visible) {
                    progressBar.opacity = fullImageView.opacity;
                } else {
                    progressBar.opacity = 1.0;
                }
                if(progressBar.progress == 100.0) {
                    progressTimeline.stop();
                }
            }
        }
    ]
};

function getProgress() : Number {
    if(fullImageView.visible) {
        return fullImageView.image.progress;
    } else {
        var totalProgress = 0.0;
        for(i in [0..((sizeof thumbImageViews) - 1)]) {
            totalProgress = totalProgress + thumbImageViews[i].image.progress;
        }
        if(totalProgress == 0) { return 0; }
        return totalProgress/(sizeof thumbImageViews); 
    }
}

public function showFullImage(index:Integer) {
    
    var tiv = thumbImageViews[index];
    fullImageView.image = Image {
        url: "http://farm{tiv.photo.farm}.static.flickr.com/{tiv.photo.server}/{tiv.photo.id}_{tiv.photo.secret}_m.jpg"
        placeholder: tiv.image
        backgroundLoading: true
    };
    
    fullImageView.show = true;
    progressBar.progress = 0.0;
    progressTimeline.playFromStart();
}

// Load image and data specified in given Photo object
function loadImage(photo: Photo, thumbImageView: ThumbImageView): Void { 
    thumbImageView.image = Image {
        url: "http://farm{photo.farm}.static.flickr.com/{photo.server}/{photo.id}_{photo.secret}_s.jpg";
        backgroundLoading: true
        placeholder: thumbImageView.image
    };
    thumbImageView.photo = photo;
}

// Update images displayed in Photo-Grid
function updateImages() {
    
    if(not loadComplete) { return; }
    
    progressBar.progress = 0.0;
    progressTimeline.playFromStart();
    
    for(i in [0..(sizeof thumbImageViews) - 1]) {
        var photoIndex = (page * (sizeof thumbImageViews)) + i;
        loadImage(photos[photoIndex], thumbImageViews[i]);
    }
    
    description = trimString("{thumbImageViews[thumbSelIndex].photo.title}", 55);
    pageButtons[page].selected = true;
}

// Application Title-Bar
var titleBar = Rectangle {
    width: bind layout.width
    height: bind layout.thumbGroupY
    fill: Color.TRANSPARENT
    visible: bind ("{__PROFILE__}" != "browser")
     onMousePressed: function(e) {
        stageDragInitialX = e.screenX - stage.x;
        stageDragInitialY = e.screenY - stage.y;
    }
     onMouseDragged: function(e) {
        stage.x = e.screenX - stageDragInitialX;
        stage.y = e.screenY - stageDragInitialY;
     }

}

// Set stage content based on status of api-key
var stageContent: Node[];

function initErrorUI() {
    
    descText = Text {
        x: bind layout.thumbGroupX + 10
        y: bind layout.thumbGroupY + 10
        font: bind layout.descFont
        wrappingWidth: bind (layout.thumbGroupW - 20)
        content: "This application requires an api key from Flickr.\nGet yours from \nhttp://developer.yahoo.com/flickr"
        fill: Color.WHITE
        smooth: true
        textOrigin: TextOrigin.TOP
    }
    
    stageContent = [
        bgRect, titleBar, closeButton, descText
    ];
}

function initDefaultUI() {
    
    descText = Text {
        x: bind layout.thumbGroupX
        y: bind layout.descTextY
        font: bind layout.descFont
        wrappingWidth: bind (layout.thumbGroupW - 10)
        content: bind description
        fill: Color.WHITE
        smooth: true
        textOrigin: TextOrigin.TOP
        clip: Rectangle {
            x: bind descText.x
            y: bind (descText.y - 2)
            width: bind (layout.width - 20)
            height: bind ( layout.thumbGroupY - descText.y - 17)
        }
    }
    
    stageContent = [
        bgRect, titleBar, nextButton, backButton, closeButton,
        descText, thumbImageViewGroup, pageButtonGroup, 
        fullImageView, progressBar
    ];

    loadImageMetadata();    
}

var loadComplete = false;
function loadImageMetadata() {
    
    println("Loading image metadata...");   
    description = "Loading Photos...";
    
    var httpRequestError: Boolean = false;
    
    // Submit HttpRequest    
    var request: HttpRequest = HttpRequest {
        
        location: "http://api.flickr.com/services/rest/?method="
            "flickr.interestingness.getList&api_key={apiKey}&per_page={layout.imageCount}"
        method: HttpRequest.GET
       
        onRead: function(bytes: Long) {
            // The toread variable is non negative only if the server provides the content length
            def loadProgress = if (request.toread > 0) "{(bytes * 100 / request.toread)}%" else "";
            description = "Loading Photos... ({loadProgress})";
        }
        
        onException: function(exception: Exception) {
            exception.printStackTrace();
            alert("Error", "{exception}");
            httpRequestError = true;
        }
            
        onResponseCode: function(responseCode:Integer) {
            if (responseCode != 200) {
                println("failed, response: {responseCode} {request.responseMessage}");
            }
        }
                        
        onInput: function(input: java.io.InputStream) {
            
            try {
                
                var parser = PhotoPullParser { 
                    onDone: function( data:Photo[] ) { 
                        photos = data; 
                        if(not httpRequestError) {
                            loadComplete = true;
                            updateImages(); 
                            focusOnGrid = true;
                        }
                    } 
                };
                parser.parse(input);
                if(parser.errorMessage.length() > 0) { 
                    alert("Error", parser.errorMessage);
                    httpRequestError = true; 
                }
                
            } finally {
                input.close();
            }
        }
    }
    
    request.start();
}

function alert(alertTitle:String, msg:String): Void {
    println(msg);
    description = trimString("{alertTitle}: {msg}", 55);
}

function initUI() {    
    // If API-Key is empty, display error
    if (apiKey == null) {
        initErrorUI();
    } else { // Retrieve photo information and display
        initDefaultUI();    
    }
}

var scene : Scene = Scene {
    width: 240
    height: 320
    content: Group {
        content: bind stageContent
        clip: Rectangle {
            width: bind layout.width
            height: bind layout.height
            arcWidth: 20
            arcHeight: 20
        }
    }
    fill: Color.TRANSPARENT
};

// Application User Interface
var stage = Stage {
    title: "Interesting Photos"
    resizable: false
    width: 240
    height: 320
    visible: false
    style: StageStyle.TRANSPARENT
    scene: bind scene
}

function run() {
    initUI();    
    stage.visible = true;
    // Set focus to background by default
    bgRect.requestFocus();
}