Commits

shemnon committed a4acabf

Pull Confrerence Schedule App from graphcs branch
- Must use Java 8, build will proactively break
- Commented out one line in com.javafx.experiments.scheduleapp.pages.TimelinePage due to new API post b61

Comments (0)

Files changed (177)

ConferenceScheduleApp/build.gradle

+apply plugin: 'javafx'
+
+buildscript {
+    if (!JavaVersion.current().isJava8Compatible()) {
+        throw new GradleException("JavaFX 8 required")
+    }
+
+    repositories {
+        mavenLocal()
+    }
+    dependencies {
+        classpath 'com.bitbucket.shemnon.javafxplugin:plugin:0.0.0-SNAPSHOT'
+    }
+}
+
+javafx {
+    javafxMainClass = 'com.javafx.experiments.scheduleapp.ConferenceScheduleApp'
+}

ConferenceScheduleApp/src/main/java/com/javafx/experiments/scheduleapp/AutoLogoutLightBox.java

+/*
+ * Copyright (c) 2008, 2012 Oracle and/or its affiliates.
+ * 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 Oracle Corporation 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 com.javafx.experiments.scheduleapp;
+
+import com.javafx.experiments.scheduleapp.control.ResizableWrappingText;
+import com.javafx.experiments.scheduleapp.data.DataService;
+import javafx.animation.Animation;
+import javafx.animation.FadeTransition;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.scene.layout.Region;
+import javafx.scene.text.Text;
+import javafx.scene.text.TextAlignment;
+import javafx.util.Duration;
+
+public class AutoLogoutLightBox extends Region {
+    private Box box;
+    private Animation fadeAnimation = null;
+    private final DataService dataService;
+
+    public AutoLogoutLightBox(DataService dataService) {
+        this.dataService = dataService;
+        getStyleClass().setAll("light-box-veil");
+        box = new Box();
+        getChildren().add(box);
+    }
+
+    public void setSecondsLeft(int secondsLeft) {
+        box.setSecondsLeft(secondsLeft);
+    }
+
+    public void show() {
+        if (fadeAnimation != null || !isVisible()) {
+            if (fadeAnimation != null) {
+                fadeAnimation.stop();
+                setVisible(true); // just to make sure
+            } else {
+                setOpacity(0);
+                setVisible(true);
+            }
+
+            FadeTransition tx = new FadeTransition(Duration.seconds(.7), this);
+            tx.setToValue(1.0);
+            tx.setOnFinished(new EventHandler<ActionEvent>() {
+                @Override public void handle(ActionEvent event) {
+                    fadeAnimation = null;
+                }
+            });
+            fadeAnimation = tx;
+            tx.play();
+        }
+    }
+
+    public void hide() {
+        if (fadeAnimation != null || isVisible()) {
+            if (fadeAnimation != null) {
+                fadeAnimation.stop();
+                setVisible(true); // just to make sure
+            } else {
+                setOpacity(1);
+                setVisible(true);
+            }
+
+            FadeTransition tx = new FadeTransition(Duration.seconds(.7), this);
+            tx.setToValue(0.0);
+            tx.setOnFinished(new EventHandler<ActionEvent>() {
+                @Override public void handle(ActionEvent event) {
+                    fadeAnimation = null;
+                    setVisible(false);
+                }
+            });
+            fadeAnimation = tx;
+            tx.play();
+        }
+    }
+
+    @Override protected double computePrefWidth(double height) {
+        final Insets insets = getInsets();
+        return insets.getLeft() + box.prefWidth(-1) + insets.getRight();
+    }
+
+    @Override protected double computePrefHeight(double width) {
+        final Insets insets = getInsets();
+        return insets.getTop() + box.prefHeight(box.prefWidth(-1)) + insets.getBottom();
+    }
+
+    @Override protected void layoutChildren() {
+        final Insets insets = getInsets();
+        double width = getWidth() - insets.getLeft() - insets.getRight();
+        double height = getHeight() - insets.getTop() - insets.getBottom();
+
+        double boxWidth = box.prefWidth(-1);
+        double boxHeight = box.prefHeight(boxWidth);
+        box.resizeRelocate((int)((width - boxWidth) / 2), (int)((height - boxHeight) / 2), boxWidth, boxHeight);
+    }
+
+    private class Box extends Region {
+        private Text message;
+
+        public Box() {
+            getStyleClass().setAll("light-box");
+            message = new ResizableWrappingText();
+            message.getStyleClass().setAll("auto-logout-text");
+            message.setTextAlignment(TextAlignment.CENTER);
+            setSecondsLeft(15);
+            getChildren().add(message);
+        }
+
+        @Override protected double computePrefWidth(double height) {
+            final Insets insets = getInsets();
+            return insets.getLeft() + 400 + insets.getRight();
+        }
+
+        @Override protected double computePrefHeight(double width) {
+            final Insets insets = getInsets();
+            return insets.getTop() + message.prefHeight(400) + insets.getBottom();
+        }
+
+        @Override protected void layoutChildren() {
+            final Insets insets = getInsets();
+            final double top = insets.getTop();
+            final double left = insets.getLeft();
+            final double width = getWidth() - left - insets.getRight();
+            final double height = getHeight() - top - insets.getBottom();
+
+            message.setWrappingWidth(width);
+            message.resizeRelocate((int) (left + .5), (int) (top + .5), (int) (width + .5), (int) (height + .5));
+        }
+
+        private void setSecondsLeft(int secondsLeft) {
+            message.setText(
+                    "Are you still there? The "+dataService.getName()+" Schedule Builder will auto-log you out in "
+                            + secondsLeft + " seconds. Touch anywhere to continue.");
+        }
+    }
+}

ConferenceScheduleApp/src/main/java/com/javafx/experiments/scheduleapp/ConferenceScheduleApp.java

+/*
+ * Copyright (c) 2008, 2012 Oracle and/or its affiliates.
+ * 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 Oracle Corporation 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 com.javafx.experiments.scheduleapp;
+
+import com.javafx.experiments.scheduleapp.control.Popover;
+import com.javafx.experiments.scheduleapp.control.VirtualKeyboard;
+import com.javafx.experiments.scheduleapp.data.DataService;
+import com.javafx.experiments.scheduleapp.data.SessionManagement;
+import com.javafx.experiments.scheduleapp.data.devoxx.DevoxxDataService;
+import com.javafx.experiments.scheduleapp.data.devoxx.TestDataService;
+import com.javafx.experiments.scheduleapp.model.SessionTime;
+import com.javafx.experiments.scheduleapp.pages.CatalogPage;
+import com.javafx.experiments.scheduleapp.pages.LoginScreen;
+import com.javafx.experiments.scheduleapp.pages.SocialPage;
+import com.javafx.experiments.scheduleapp.pages.SpeakersPage;
+import com.javafx.experiments.scheduleapp.pages.TimelinePage;
+import com.javafx.experiments.scheduleapp.pages.TracksPage;
+import com.javafx.experiments.scheduleapp.pages.VenuesPage;
+import com.sun.glass.ui.Screen;
+import com.sun.javafx.tk.TKSceneListener;
+import com.sun.javafx.tk.quantum.GlassScene;
+import java.lang.reflect.Field;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.Timer;
+import java.util.TimerTask;
+import javafx.animation.Animation;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.animation.TranslateTransition;
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.ActionEvent;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.geometry.VPos;
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.control.TextInputControl;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.image.Image;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.ScrollEvent;
+import javafx.scene.input.TouchEvent;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.ImagePattern;
+import javafx.scene.paint.Paint;
+import javafx.scene.text.Text;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+
+public class ConferenceScheduleApp extends Application {
+    private static final String os = System.getProperty("os.name");
+    public static final boolean IS_BEAGLE = "Linux".equals(os) && Boolean.getBoolean("com.sun.javafx.isEmbedded");
+    public static final boolean IS_MAC = "Mac OS X".equals(os);
+    public static final boolean IS_WINDOWS = os.startsWith("Windows");
+    public static final boolean DISABLE_AUTO_LOGOUT = "true".equalsIgnoreCase(System.getProperty("disable.auto.logout"));
+    public static final boolean IS_TESTING_MODE = "true".equalsIgnoreCase(System.getProperty("test.mode"));
+    public static final boolean IS_VK_DISABLED = Boolean.getBoolean("no.vk");
+
+    private static ConferenceScheduleApp INSTANCE;
+    private Popover centralPopover;
+    private PageContainer pageContainer;
+    private StackPane root;
+    private LoginScreen loginScreen;
+    private TimelinePage TIMELINE_PAGE;
+    private CatalogPage CATALOG_PAGE;
+    private final SessionManagement SESSION_MANAGEMENT = new SessionManagement();
+    private long startTime;
+    private long lastFrame;
+    private int frames =0;
+    private DataService dataService;
+    private Scene scene;
+    private ToggleButton loginLogoutButton;
+    private Animation loginLogoutAnimation;
+    private Timeline automatedTimer;
+    private Timer autoLogoutTimer;
+    private volatile long countdownTimerStart;
+    private AutoLogoutLightBox lightBox;
+    private VirtualKeyboard keyboard;
+    private Animation keyboardSlideAnimation;
+    private TKSceneListener sceneListener;
+    private Text clock = new Text();
+    private DateFormat dateFormat = new SimpleDateFormat("E hh:mm a");
+
+    @Override public void start(Stage stage) throws Exception {
+        INSTANCE = this;
+        if(IS_TESTING_MODE) System.out.println("==============================================\n    WARNING: IN TEST MODE\n==============================================");
+        
+        // create data service
+        dataService = IS_TESTING_MODE ? new TestDataService() : new DevoxxDataService(7);
+        
+        centralPopover = new Popover();
+        centralPopover.setPrefWidth(400);
+        lightBox = new AutoLogoutLightBox(dataService);
+        lightBox.setVisible(false);
+        keyboard = new VirtualKeyboard();
+        loginLogoutButton = new ToggleButton();
+        
+        // calculate window size
+        final double width = IS_BEAGLE ? Screen.getMainScreen().getWidth() : 1024;
+        final double height = IS_BEAGLE ? Screen.getMainScreen().getHeight(): 600;
+
+        // create pages
+        CATALOG_PAGE = new CatalogPage(centralPopover, dataService);
+        TIMELINE_PAGE = new TimelinePage(centralPopover, dataService);
+        pageContainer = new PageContainer(centralPopover, lightBox,
+            TIMELINE_PAGE,
+            CATALOG_PAGE,
+            new SocialPage(dataService),
+            new SpeakersPage(centralPopover, dataService),
+            new VenuesPage(centralPopover, dataService),
+            new TracksPage(centralPopover, dataService)
+        );
+        pageContainer.setVisible(false);
+
+        clock.setId("Clock");
+        clock.setText(dateFormat.format(new Date()));
+        clock.setTextOrigin(VPos.TOP);
+        Timer clockTimer = new Timer(true);
+        clockTimer.scheduleAtFixedRate(new TimerTask() {
+            @Override public void run() {
+                Platform.runLater(new Runnable() {
+                    @Override public void run() {
+                         clock.setText(dateFormat.format(new Date()));
+                    }
+                });
+            }
+        }, 20000, 20000);
+
+        // create login/logout button
+        loginLogoutButton.setId("LoginLogout");
+        loginLogoutButton.getStyleClass().clear();
+        loginLogoutButton.resize(75, 31);
+        loginLogoutButton.setOnMouseClicked(new EventHandler<MouseEvent>() {
+            @Override public void handle(MouseEvent event) {
+                showLoginScreen();
+            }
+        });
+        pageContainer.getChildren().addAll(clock, loginLogoutButton);
+
+        pageContainer.getChildren().addAll(centralPopover, lightBox);
+
+        // create login screen
+        loginScreen = new LoginScreen(dataService, height < 1000);
+        
+        // create root
+        root = new StackPane() {
+            @Override protected void layoutChildren() {
+                final double w = getWidth();
+                final double h = getHeight();
+                super.layoutChildren();
+                keyboard.resizeRelocate(0, h, w, w * (3.0/11.0));
+                clock.setX(w - 240);
+                clock.setY(9);
+                loginLogoutButton.setLayoutX(w-67-12);
+                loginLogoutButton.setLayoutY(5);
+            }
+        };
+        root.getChildren().addAll(pageContainer, loginScreen, keyboard);
+
+        // create scene
+        scene = new Scene(root, width, height);
+
+        if (!IS_VK_DISABLED) {
+            keyboard.setOnAction(new EventHandler<KeyEvent>() {
+                @Override public void handle(KeyEvent event) {
+                    if (sceneListener == null) {
+                        try {
+                            GlassScene peer = (GlassScene) scene.impl_getPeer();
+                            Field f = GlassScene.class.getDeclaredField("sceneListener");
+                            f.setAccessible(true);
+                            sceneListener = (TKSceneListener) f.get(peer);
+                        } catch (Exception e) {
+                            e.printStackTrace();
+                        }
+                    }
+
+                    // TODO not sure how to implement
+                    sceneListener.keyEvent(
+                            (EventType<KeyEvent>)event.getEventType(),
+                            event.getCode().impl_getCode(),
+                            event.getCharacter().toCharArray(),
+                            event.isShiftDown(), false, false, false);
+                }
+            });
+            scene.focusOwnerProperty().addListener(new ChangeListener<Node>() {
+                @Override public void changed(ObservableValue<? extends Node> observable, Node oldValue, Node newValue) {
+                    boolean isTextOwner = false;
+                    Parent parent = newValue instanceof Parent ? (Parent) newValue : null;
+                    while (parent != null) {
+                        if (parent instanceof TextInputControl) {
+                            isTextOwner = true;
+                            break;
+                        }
+                        parent = parent.getParent();
+                    }
+
+                    if (isTextOwner && keyboard.getTranslateY() == 0) {
+                        // The focus on a text input control and therefore we must show the keyboard
+                        if (keyboardSlideAnimation != null) {
+                            keyboardSlideAnimation.stop();
+                        }
+                        TranslateTransition tx = new TranslateTransition(Duration.seconds(.4), keyboard);
+                        tx.setToY(-(scene.getWidth() * (3.0 / 11.0)));
+                        keyboardSlideAnimation = tx;
+                        keyboardSlideAnimation.play();
+                    } else if (!isTextOwner && keyboard.getTranslateY() != 0) {
+                        if (keyboardSlideAnimation != null) {
+                            keyboardSlideAnimation.stop();
+                        }
+                        TranslateTransition tx = new TranslateTransition(Duration.seconds(.4), keyboard);
+                        tx.setToY(0);
+                        keyboardSlideAnimation = tx;
+                        keyboardSlideAnimation.play();
+                    }
+
+                    if (newValue != null) {
+                        VirtualKeyboard.Type type = (VirtualKeyboard.Type) newValue.getProperties().get("vkType");
+                        keyboard.setType(type == null ? VirtualKeyboard.Type.TEXT : type);
+                    }
+                }
+            });
+        }
+
+        if (IS_BEAGLE) {
+            TouchScrollEventSynthesizer ises = new TouchScrollEventSynthesizer(scene);
+        }
+        // beagle is really slow with background textures so use color for beagle
+        Paint background = IS_BEAGLE ? Color.web("#e8eae8") :
+                new ImagePattern(
+                    new Image(getClass().getResource("images/rough_diagonal.png").toExternalForm()),
+                    0,0,255,255,false);
+        scene.setFill(background);
+        scene.getStylesheets().add(
+                getClass().getResource("SchedulerStyleSheet.css").toExternalForm());
+        if(!IS_BEAGLE) {
+            scene.getStylesheets().add(
+                    getClass().getResource("SchedulerStyleSheet-Desktop.css").toExternalForm());
+        }
+
+        EventHandler<Event> resetAutoLogoutTimerHandler = new EventHandler<Event>() {
+            @Override public void handle(Event event) {
+                countdownTimerStart = System.currentTimeMillis();
+                lightBox.hide();
+            }
+        };
+        scene.addEventFilter(MouseEvent.MOUSE_PRESSED, resetAutoLogoutTimerHandler);
+        scene.addEventFilter(MouseEvent.MOUSE_CLICKED, resetAutoLogoutTimerHandler);
+        scene.addEventFilter(KeyEvent.KEY_PRESSED, resetAutoLogoutTimerHandler);
+        scene.addEventFilter(KeyEvent.KEY_RELEASED, resetAutoLogoutTimerHandler);
+        scene.addEventFilter(ScrollEvent.ANY, resetAutoLogoutTimerHandler);
+        scene.addEventFilter(TouchEvent.ANY, resetAutoLogoutTimerHandler);
+
+        stage.setScene(scene);
+        // show stage
+        stage.show();
+    }
+
+    public static ConferenceScheduleApp getInstance() { return INSTANCE; }
+
+    public SessionManagement getSessionManagement() {
+        return SESSION_MANAGEMENT;
+    }
+
+    public void showLoginScreen() {
+        for (com.javafx.experiments.scheduleapp.model.Event event : dataService.getEvents()) {
+            SessionTime time = event.getSessionTime();
+            if (time != null) time.setEvent(null);
+        }
+        dataService.getEvents().clear();
+
+        if (autoLogoutTimer != null) {
+            autoLogoutTimer.cancel();
+            autoLogoutTimer = null;
+            if (lightBox.isVisible()) {
+                lightBox.setVisible(false);
+            }
+        }
+
+        centralPopover.hide();
+
+        loginScreen.setOpacity(0);
+        loginScreen.setVisible(true);
+        loginScreen.reset();
+        // logout
+        SESSION_MANAGEMENT.logout();
+        pageContainer.reset();
+        if (loginLogoutAnimation != null) {
+            loginLogoutAnimation.stop();
+            pageContainer.setVisible(true);
+        }
+
+        loginLogoutAnimation = new Timeline(
+                new KeyFrame(
+                    Duration.millis(800), 
+                    new EventHandler<ActionEvent>() {
+                        @Override public void handle(ActionEvent event) {
+                            pageContainer.setVisible(false);
+                            pageContainer.setCache(false);
+                            loginScreen.setCache(false);
+                            loginLogoutAnimation = null;
+                        }
+                    },
+                    new KeyValue(loginScreen.opacityProperty(), 1d)
+                )
+        );
+        loginLogoutAnimation.play();
+    }
+    
+    public void hideLoginScreen() {
+        final boolean isGuest = SESSION_MANAGEMENT.isGuestProperty().get();
+        pageContainer.gotoPage(isGuest ? CATALOG_PAGE : TIMELINE_PAGE, false);
+        pageContainer.setVisible(true);
+        // update login button state
+        loginLogoutButton.setSelected(!isGuest);
+        if (loginLogoutAnimation != null) {
+            loginLogoutAnimation.stop();
+            loginScreen.setVisible(true);
+        }
+
+        loginLogoutAnimation = new Timeline(
+                new KeyFrame(
+                    Duration.millis(800), 
+                    new EventHandler<ActionEvent>() {
+                        @Override public void handle(ActionEvent event) {
+                            loginScreen.setVisible(false);
+                            loginScreen.setCache(false);
+                            pageContainer.setCache(false);
+                            loginLogoutAnimation = null;
+                        }
+                    },
+                    new KeyValue(loginScreen.opacityProperty(), 0d)
+                )
+        );
+        loginLogoutAnimation.play();
+
+        if (!DISABLE_AUTO_LOGOUT) {
+            countdownTimerStart = System.currentTimeMillis();
+            autoLogoutTimer = new Timer("Auto Logout Timer", true);
+            autoLogoutTimer.scheduleAtFixedRate(new TimerTask() {
+                @Override public void run() {
+                    long currentTime = System.currentTimeMillis();
+                    long diff = currentTime - countdownTimerStart;
+                    if (diff > 60000) {
+                        Platform.runLater(new Runnable() {
+                            @Override public void run() {
+                                showLoginScreen();
+                            }
+                        });
+                    } else if (diff > 45000) {
+                        long remaining = 60000 - diff;
+                        final int seconds = (int) (remaining / 1000.0);
+                        Platform.runLater(
+                                new Runnable() {
+                                    @Override public void run() {
+                                        lightBox.setSecondsLeft(seconds);
+                                        lightBox.show();
+                                    }
+                                });
+                    }
+                }
+            }, 1000, 1000);
+        }
+    }
+
+
+    public static void main(String[] args) throws  Exception {
+        Locale.setDefault(Locale.US);
+        TimeZone.setDefault(TimeZone.getTimeZone("PST"));
+        launch(args);
+    }
+}

ConferenceScheduleApp/src/main/java/com/javafx/experiments/scheduleapp/Page.java

+/*
+ * Copyright (c) 2008, 2012 Oracle and/or its affiliates.
+ * 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 Oracle Corporation 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 com.javafx.experiments.scheduleapp;
+
+import javafx.scene.control.Label;
+import javafx.scene.layout.Region;
+import com.javafx.experiments.scheduleapp.data.DataService;
+
+public abstract class Page extends Region {
+    
+    private final String name;
+    protected final DataService dataService;
+
+    public Page(String name, DataService dataService) {
+        this.name = name;
+        this.dataService = dataService;
+        Label temp = new Label(name);
+        temp.setStyle("-fx-font-size: 30px;");
+        getChildren().add(temp);
+    }
+
+    public String getName() {
+        return name;
+    }
+    
+    public void pageTabClicked() {
+    }
+
+    public abstract void reset();
+}

ConferenceScheduleApp/src/main/java/com/javafx/experiments/scheduleapp/PageContainer.java

+/*
+ * Copyright (c) 2008, 2012 Oracle and/or its affiliates.
+ * 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 Oracle Corporation 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 com.javafx.experiments.scheduleapp;
+
+import static com.javafx.experiments.scheduleapp.Theme.*;
+import com.javafx.experiments.scheduleapp.control.Popover;
+import java.util.HashMap;
+import java.util.Map;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.TimelineBuilder;
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.event.EventHandler;
+import javafx.geometry.Bounds;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.ImagePattern;
+import javafx.scene.shape.Rectangle;
+import javafx.util.Duration;
+
+/**
+ * A custom page container like a TabView but with special visuals and animation.
+ */
+public class PageContainer extends Pane {
+    private static final Color TEXT_SELECTED_COLOR = Color.WHITE;
+    // model
+    private final ObservableList<Page> pages = FXCollections.observableArrayList();
+    private Page currentPage = null;
+    // ui
+    private HBox header = new HBox();
+    private Rectangle selectionBox = new Rectangle();
+    private Map<Page,Label> titlesMap = new HashMap<Page, Label>();
+    private ImageView headerShadow = new ImageView(
+                new Image(getClass().getResource("images/header-shadow.png").toExternalForm()));
+    private ImageView headerArrow = new ImageView(
+                new Image(getClass().getResource("images/header-arrow.png").toExternalForm()));
+    private final Popover popover;
+    private final AutoLogoutLightBox lightBox;
+
+    public PageContainer(Popover popover, AutoLogoutLightBox lightBox, final Page ... pages) {
+        this.pages.addAll(pages);
+        this.popover = popover;
+        this.lightBox = lightBox;
+        // build ui
+        header.setId("page-container-header");
+        header.setFillHeight(true);
+        headerShadow.setMouseTransparent(true);
+        headerArrow.setMouseTransparent(true);
+//        selectionBox.setFill(new ImagePattern(
+//                new Image(getClass().getResource("images/rough_diagonal_blue.jpg").toExternalForm()),
+//                0,0,255,255,false));
+        selectionBox.setFill( new ImagePattern(
+                new Image(getClass().getResource("images/rough_diagonal_blue.jpg").toExternalForm()),
+                0,0,255,255,false));
+        selectionBox.setManaged(false);
+        header.getChildren().add(selectionBox);
+        getChildren().addAll(header,headerShadow, headerArrow);
+        // add all pages
+        getChildren().addAll(pages);
+        for (Page page: pages) {
+            page.setVisible(false);
+        }
+        // do first rebuild and listen to changes in available pages
+        rebuild();
+        this.pages.addListener(new ListChangeListener<Page>() {
+            @Override public void onChanged(Change<? extends Page> change) {
+                rebuild();
+            }
+        });
+        // goto first page, runLater because we want this to happen after first layout
+        Platform.runLater(new Runnable() {
+            @Override public void run() {
+                gotoPage(pages[0], false);
+            }
+        });
+    }
+    
+    private void rebuild() {
+        if (header.getChildren().size() > 1) {
+            header.getChildren().remove(1, header.getChildren().size());
+            titlesMap.clear();
+        }
+        for(final Page page: pages) {
+            Label title = new Label(page.getName());
+            titlesMap.put(page,title);
+            title.setMaxHeight(Double.MAX_VALUE);
+            title.getStyleClass().add("page-container-header-title");
+            title.setPickOnBounds(true);
+            title.setOnMouseClicked(new EventHandler<MouseEvent>() {
+                @Override public void handle(MouseEvent t) {
+                    System.out.println("CLICKED ON PAGE BUTTON FOR "+page.getName());
+                    if (currentPage != page) {
+                        gotoPage(page, true);
+                    }
+                    page.pageTabClicked();
+                }
+            });
+            header.getChildren().add(title);
+        }
+    }
+    
+    public void gotoPage(Page page, boolean animate) {
+        System.out.println("CHANGING TO PAGE --> "+page.getName());
+        final Label newTitleLabel = titlesMap.get(page);
+        final Bounds newPageTitleBounds = newTitleLabel.getBoundsInParent();
+        if (currentPage != null && animate) {
+            final Label currentTitlelabel = titlesMap.get(currentPage);
+            final Bounds currentPageTitleBounds = currentTitlelabel.getBoundsInParent();
+            TimelineBuilder.create()
+                .keyFrames(
+                    new KeyFrame(Duration.ZERO, 
+                        new KeyValue(selectionBox.xProperty(), currentPageTitleBounds.getMinX()),
+                        new KeyValue(selectionBox.yProperty(), currentPageTitleBounds.getMinY()),
+                        new KeyValue(selectionBox.widthProperty(), currentPageTitleBounds.getWidth()),
+                        new KeyValue(selectionBox.heightProperty(), currentPageTitleBounds.getHeight()),
+                        new KeyValue(newTitleLabel.textFillProperty(), DARK_GREY),
+                        new KeyValue(currentTitlelabel.textFillProperty(), TEXT_SELECTED_COLOR),
+                        new KeyValue(headerArrow.layoutXProperty(), headerArrow.getLayoutX())
+                    ),
+                    new KeyFrame(Duration.seconds(.3), 
+                        new KeyValue(selectionBox.xProperty(), newPageTitleBounds.getMinX(), Interpolator.EASE_BOTH),
+                        new KeyValue(selectionBox.yProperty(), newPageTitleBounds.getMinY(), Interpolator.EASE_BOTH),
+                        new KeyValue(selectionBox.widthProperty(), newPageTitleBounds.getWidth(), Interpolator.EASE_BOTH),
+                        new KeyValue(selectionBox.heightProperty(), newPageTitleBounds.getHeight(), Interpolator.EASE_BOTH),
+                        new KeyValue(newTitleLabel.textFillProperty(), TEXT_SELECTED_COLOR, Interpolator.EASE_BOTH),
+                        new KeyValue(currentTitlelabel.textFillProperty(), DARK_GREY, Interpolator.EASE_BOTH),
+                        new KeyValue(headerArrow.layoutXProperty(), newPageTitleBounds.getMinX() + (newPageTitleBounds.getWidth()/2) - 6, Interpolator.EASE_BOTH)
+                    )
+                )
+                .build().play();
+            // hide current page
+            currentPage.setVisible(false);
+        } else {
+            selectionBox.setX(newPageTitleBounds.getMinX());
+            selectionBox.setY(newPageTitleBounds.getMinY());
+            selectionBox.setWidth(newPageTitleBounds.getWidth());
+            selectionBox.setHeight(newPageTitleBounds.getHeight());
+            headerArrow.setLayoutX(newPageTitleBounds.getMinX() + (newPageTitleBounds.getWidth()/2) - 6);
+            if (currentPage != null) {
+                // hide current page
+                currentPage.setVisible(false);
+                // change current pages title back to dark grey
+                titlesMap.get(currentPage).setTextFill(DARK_GREY);
+            }
+            newTitleLabel.setTextFill(TEXT_SELECTED_COLOR);
+        }
+//        if(getChildren().size() == 3) {
+//            getChildren().add(page);
+//        } else {
+//            getChildren().set(3,page);
+//        }
+        page.setVisible(true);
+        currentPage = page;
+    }
+
+    @Override protected void layoutChildren() {
+        final double w = getWidth();
+        final double h = getHeight();
+        header.resize(w, 40);
+        headerShadow.setFitWidth(w);
+        for(Page page: pages) {
+            page.resizeRelocate(0, 40, w, h-40);
+        }
+
+        Scene scene = getScene();
+        double width = popover.prefWidth(-1);
+        popover.setLayoutX((int) ((scene.getWidth() - width) / 2));
+        popover.setLayoutY(50);
+        if (popover.isVisible()) {
+            popover.autosize();
+        }
+        lightBox.resizeRelocate(0, 0, w, h);
+    }
+    
+    public ObservableList<Page> getPages() {
+        return pages;
+    }
+
+    public void reset() {
+        for (Page page : pages) {
+            page.reset();
+        }
+    }
+}

ConferenceScheduleApp/src/main/java/com/javafx/experiments/scheduleapp/PlatformIntegration.java

+/*
+ * Copyright (c) 2008, 2012 Oracle and/or its affiliates.
+ * 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 Oracle Corporation 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 com.javafx.experiments.scheduleapp;
+
+import static com.javafx.experiments.scheduleapp.ConferenceScheduleApp.*;
+import com.javafx.experiments.scheduleapp.data.TwitterJson;
+import com.javafx.experiments.scheduleapp.model.Event;
+import com.javafx.experiments.scheduleapp.model.Session;
+import com.javafx.experiments.scheduleapp.model.Tweet;
+import com.javafx.experiments.scheduleapp.pages.SocialPage;
+import java.awt.Desktop;
+import java.net.URI;
+import java.util.Date;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javafx.util.Callback;
+
+/**
+ * class giving access to platform integration
+ */
+public class PlatformIntegration {
+    
+    /**
+     * Check if platform supports opening URLs in the system browser
+     * 
+     * @return true if system can open URLs
+     */
+    public static boolean supportsSystemCalendar() {
+        return false; 
+    }
+    
+    /**
+     * Read all events from the system calendar between the given start and 
+     * end dates.
+     * 
+     * @param start The start of range to find events from
+     * @param end   The end of range to find events to
+     * @return List of all events found
+     */
+    public static List<Event> readEvents(List<Session> sessions, Date start, Date end) {
+        throw new UnsupportedOperationException();
+    }
+    
+    /**
+     * Write the given list of events into the system calendar.
+     * 
+     * @param events List of events to save into calendar
+     */
+    public static void writeEvents(List<Event> events) {
+        throw new UnsupportedOperationException();
+    }
+    
+    /**
+     * Delete the given list of events from the system calendar.
+     * 
+     * @param events List of events to delete
+     */
+    public static void deleteEvents(List<Event> events) {
+        throw new UnsupportedOperationException();
+    }
+    
+    /**
+     * Check if platform supports opening URLs in the system browser
+     * 
+     * @return true if system can open URLs
+     */
+    public static boolean supportsOpeningUrls() {
+        return IS_MAC || IS_WINDOWS; 
+    }
+    
+    /**
+     * Open the given URL in the system web browser.
+     * 
+     * @param url 
+     */
+    public static void openUrl(String url) {
+        if (IS_MAC) {
+            try {
+                Process p = Runtime.getRuntime().exec(new String[]{"open",url});
+            } catch (Exception ex) {
+                Logger.getLogger(SocialPage.class.getName()).log(Level.SEVERE, null, ex);
+            }
+        } else if(IS_WINDOWS) {
+            try {
+                Desktop.getDesktop().browse(new URI(url));
+            } catch (Exception ex) {
+                Logger.getLogger(SocialPage.class.getName()).log(Level.SEVERE, null, ex);
+            }
+        } else {
+            throw new UnsupportedOperationException();
+        }
+    }
+    
+    /**
+     * Get all tweets returned by the twitter API call with the given URL.
+     * 
+     * @param url
+     * @param gotTweetCallBack 
+     */
+    public static void getTweetsForQuery(String url, final Callback<Tweet[],Void> gotTweetCallBack) {
+        TwitterJson.getTweetsForQuery(url, gotTweetCallBack);
+    }
+    
+    /**
+     * Check if platform supports opening URLs in the system browser
+     * 
+     * @return true if system can open URLs
+     */
+    public static boolean supportsSendingTweets() {
+        return false; 
+    }
+    
+    /**
+     * Open a platform native send tweet dialog with the given initial text.
+     * 
+     * @param text The initial text for tweet
+     */
+    public static void tweet(String text) {
+        throw new UnsupportedOperationException();
+    }
+    
+    /**
+     * Check if platform supports showing map routes
+     * 
+     * @return true if system can show maps
+     */
+    public static boolean supportsMapRouting() {
+        return false; 
+    }
+    
+    /**
+     * Open system maps application and show route from current location to the
+     * given address.
+     * 
+     * @param address The address to show route to
+     */
+    public static void openMapRoute(final String address) {
+        throw new UnsupportedOperationException();
+    }
+}

ConferenceScheduleApp/src/main/java/com/javafx/experiments/scheduleapp/Theme.java

+/*
+ * Copyright (c) 2008, 2012 Oracle and/or its affiliates.
+ * 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 Oracle Corporation 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 com.javafx.experiments.scheduleapp;
+
+import javafx.scene.image.Image;
+import javafx.scene.paint.Color;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontWeight;
+
+/**
+ * This is a very simple theme of constants for styles used though out the 
+ * application. This allows us to avoid the cost of CSS.
+ */
+public class Theme {
+    public static final String DEFAULT_FONT_NAME = "Helvetica";
+    public static final Image RIGHT_ARROW = new Image(
+            Theme.class.getResource("images/popover-arrow.png").toExternalForm());
+    public static final Image STAR = new Image(
+            Theme.class.getResource("images/star.png").toExternalForm());
+    public static Image SHADOW_PIC = new Image(
+            Theme.class.getResource("images/pic-shadow.png").toExternalForm());
+    public static Image DUKE_48 = new Image(
+            Theme.class.getResource("images/duke48.png").toExternalForm());
+    public static final Image TICK_IMAGE = new Image(
+            Theme.class.getResource("images/tick.png").toExternalForm());
+    public static final Font HUGE_FONT = Font.font(DEFAULT_FONT_NAME, FontWeight.BOLD, 40);
+    public static final Font LARGE_FONT = Font.font(DEFAULT_FONT_NAME, FontWeight.BOLD, 20);
+    public static final Font LARGE_LIGHT_FONT = Font.font(DEFAULT_FONT_NAME, FontWeight.NORMAL, 18);
+    public static final Font BASE_FONT = Font.font(DEFAULT_FONT_NAME, FontWeight.BOLD, 14);
+    public static final Font LIGHT_FONT = Font.font(DEFAULT_FONT_NAME, FontWeight.NORMAL, 14);
+    public static final Font SMALL_FONT = Font.font(DEFAULT_FONT_NAME, FontWeight.BOLD, 10);
+    public static final Color BLUE = Color.web("#00a8cc");
+    public static final Color PINK = Color.web("#ea0068");
+    public static final Color GRAY = Color.web("#5f5f5f");
+    public static final Color VLIGHT_GRAY = Color.web("#9b9b9b");
+    public static final Color DARK_GREY = Color.web("#363636");
+}

ConferenceScheduleApp/src/main/java/com/javafx/experiments/scheduleapp/TouchClickedEventAvoider.java

+/*
+ * Copyright (c) 2008, 2012 Oracle and/or its affiliates.
+ * 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 Oracle Corporation 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 com.javafx.experiments.scheduleapp;
+
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.ScrollEvent;
+
+public class TouchClickedEventAvoider {
+    boolean scrolling;
+    double pressX, pressY;
+    
+    public TouchClickedEventAvoider(Node node) {
+        node.addEventFilter(ActionEvent.ACTION, new EventHandler<ActionEvent>() {
+            public void handle(ActionEvent event) {
+                if (scrolling) event.consume();
+            }
+        });
+        
+        node.addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
+            public void handle(MouseEvent event) {
+                pressX = event.getScreenX();
+                pressY = event.getScreenY();
+            }
+        });
+        node.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
+            public void handle(MouseEvent event) {
+                if (scrolling) event.consume();
+            }
+        });
+        node.addEventFilter(ScrollEvent.SCROLL_STARTED, new EventHandler<ScrollEvent>() {
+            public void handle(ScrollEvent event) {
+                scrolling = true;
+            }
+        });
+        node.addEventFilter(ScrollEvent.SCROLL_FINISHED, new EventHandler<ScrollEvent>() {
+            public void handle(ScrollEvent event) {
+                scrolling = false;
+            }
+        });
+    }
+
+}

ConferenceScheduleApp/src/main/java/com/javafx/experiments/scheduleapp/TouchScrollEventSynthesizer.java

+/*
+ * Copyright (c) 2008, 2012 Oracle and/or its affiliates.
+ * 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 Oracle Corporation 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 com.javafx.experiments.scheduleapp;
+
+import javafx.animation.*;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.scene.Scene;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.ScrollEvent;
+import javafx.util.Duration;
+
+public class TouchScrollEventSynthesizer implements EventHandler {
+    private static final int INERTIA_DURATION = 2400;
+    private static final double CLICK_THRESHOLD = 20;
+    private static final double CLICK_TIME_THRESHOLD = Integer.parseInt(System.getProperty("click", "400"));
+    static {
+        System.out.println("CLICK_TIME_THRESHOLD = " + CLICK_TIME_THRESHOLD);
+    }
+    private long startDrag;
+    private long lastDrag;
+    private long lastDragDelta;
+    private int startDragX;
+    private int startDragY;
+    private int lastDragX;
+    private int lastDragY;
+    private int lastDragStepX;
+    private int lastDragStepY;
+    private double dragVelocityX;
+    private double dragVelocityY;
+    private boolean clickThresholdBroken;
+    private Timeline inertiaTimeline = null;
+    private long lastClickTime = -1;
+    private boolean isFiredByMe = false;
+
+    public TouchScrollEventSynthesizer(Scene scene) {
+        scene.addEventFilter(MouseEvent.ANY, this);
+        scene.addEventFilter(ScrollEvent.ANY, this);
+    }
+
+    @Override public void handle(final Event e) {
+        if (isFiredByMe) return;
+        if (e instanceof ScrollEvent) {
+            final ScrollEvent se = (ScrollEvent)e;
+//            System.out.println("SCROLL touch = "+se.getTouchCount()+" e = "+se.getEventType()+"  dx="+se.getDeltaX()+"  dy="+se.getDeltaY()+"  tdx="+se.getTotalDeltaX()+"  tdy="+se.getTotalDeltaY());
+            if (se.getTouchCount() != 0) se.consume();
+        } else if (e instanceof MouseEvent) {
+            final MouseEvent me = (MouseEvent)e;
+//            System.out.println("MOUSE "+e.getEventType());
+            if (e.getEventType() == MouseEvent.MOUSE_PRESSED) {
+                lastDragX = startDragX = (int)me.getX();
+                lastDragY = startDragY = (int)me.getY();
+                lastDrag = startDrag = System.currentTimeMillis();
+                lastDragDelta = 0;
+                if(inertiaTimeline != null) inertiaTimeline.stop();
+                clickThresholdBroken = false;
+            } else if (e.getEventType() == MouseEvent.MOUSE_DRAGGED) {
+                // Delta of this drag vs. last drag location (or start)
+                lastDragStepX = (int)me.getX() - lastDragX;
+                lastDragStepY = (int)me.getY() - lastDragY;
+
+                // Duration of this drag step.
+                lastDragDelta = System.currentTimeMillis() - lastDrag;
+
+                // Velocity of last drag increment.
+                dragVelocityX = (double)lastDragStepX/(double)lastDragDelta;
+                dragVelocityY = (double)lastDragStepY/(double)lastDragDelta;
+
+                // Snapshot of this drag event.
+                lastDragX = (int)me.getX();
+                lastDragY = (int)me.getY();
+                lastDrag = System.currentTimeMillis();
+
+                // Calculate distance so far -- have we dragged enough to scroll?
+                final int dragX = (int)me.getX() - startDragX;
+                final int dragY = (int)me.getY() - startDragY;
+                double distance = Math.abs(Math.sqrt((dragX*dragX) + (dragY*dragY)));
+
+                int scrollDistX = lastDragStepX;
+                int scrollDistY = lastDragStepY;
+                if (!clickThresholdBroken && distance > CLICK_THRESHOLD) {
+                    clickThresholdBroken = true;
+                    scrollDistX = dragX;
+                    scrollDistY = dragY;
+                }
+                if (clickThresholdBroken) {
+                    // TODO
+                    Event.fireEvent(e.getTarget(), ScrollEvent.impl_scrollEvent(
+                            ScrollEvent.SCROLL,
+                            scrollDistX, scrollDistY,
+                            scrollDistX, scrollDistY,
+                            ScrollEvent.HorizontalTextScrollUnits.NONE, 0,
+                            ScrollEvent.VerticalTextScrollUnits.NONE, 0,
+                            0,
+                            me.getX(), me.getY(),
+                            me.getSceneX(), me.getSceneY(),
+                            me.isShiftDown(), me.isControlDown(), me.isAltDown(), me.isMetaDown(),
+                            true, false));
+                }
+            } else if (e.getEventType() == MouseEvent.MOUSE_RELEASED) {
+                handleRelease(me);
+            } else if (e.getEventType() == MouseEvent.MOUSE_CLICKED) {
+                final long time = System.currentTimeMillis();
+//                System.out.println("CLICKED   clickThresholdBroken="+clickThresholdBroken+"   timeSinceLast = "+ (time-lastClickTime)+" CONSUMED="+((time-lastClickTime) < CLICK_TIME_THRESHOLD));
+                if (clickThresholdBroken || (lastClickTime != -1 && (time-lastClickTime) < CLICK_TIME_THRESHOLD)) e.consume();
+                lastClickTime = time;
+            }
+        }
+    }
+
+    private void handleRelease(final MouseEvent me) {
+        if (clickThresholdBroken) {
+            // Calculate last instantaneous velocity. User may have stopped moving
+            // before they let go of the mouse.
+            final long time = System.currentTimeMillis() - lastDrag;
+            dragVelocityX = (double)lastDragStepX/(time + lastDragDelta);
+            dragVelocityY = (double)lastDragStepY/(time + lastDragDelta);
+
+            // determin if click or drag/flick
+            final int dragX = (int)me.getX() - startDragX;
+            final int dragY = (int)me.getY() - startDragY;
+
+            // calculate complete time from start to end of drag
+            final long totalTime = System.currentTimeMillis() - startDrag;
+
+            // if time is less than 300ms then considered a quick flick and whole time is used
+            final boolean quick = totalTime < 300;
+
+            // calculate velocity
+            double velocityX = quick ? (double)dragX / (double)totalTime : dragVelocityX; // pixels/ms
+            double velocityY = quick ? (double)dragY / (double)totalTime : dragVelocityY; // pixels/ms
+
+//            System.out.printf("dragVelocityX = %f, dragVelocityY = %f\n", dragVelocityX, dragVelocityY);
+//            System.out.printf("velocityX = %f, dragX = %d; velocityY = %f, dragY = %d; totalTime = %d\n",
+//                    velocityX, dragX, velocityY, dragY, totalTime);
+
+            // calculate distance we would travel at this speed for INERTIA_DURATION ms of travel
+            final int distanceX = (int)(velocityX * INERTIA_DURATION); // distance
+            final int distanceY = (int)(velocityY * INERTIA_DURATION); // distance
+            //
+            DoubleProperty animatePosition = new SimpleDoubleProperty() {
+                double lastMouseX = me.getX();
+                double lastMouseY = me.getY();
+                @Override protected void invalidated() {
+                    final double mouseX = me.getX() + (distanceX * get());
+                    final double mouseY = me.getY() + (distanceY * get());
+                    final double dragStepX = mouseX - lastMouseX;
+                    final double dragStepY = mouseY - lastMouseY;
+
+                    if (Math.abs(dragStepX) >= 1.0 || Math.abs(dragStepY) >= 1.0) {