/*
 * Copyright (c) 2010, 2011, Oracle  and/or its affiliates. All rights reserved.
 */

package com.sun.javafx.tk.quantum;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import javafx.application.ConditionalFeature;
import javafx.geometry.Dimension2D;
import javafx.scene.image.Image;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.InputMethodEvent;
import javafx.scene.input.InputMethodRequests;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.PathElement;
import javafx.scene.shape.QuadCurveTo;
import javafx.scene.shape.SVGPath;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.StageStyle;

import com.sun.glass.ui.Application;
import com.sun.glass.ui.ClipboardAssistance;
import com.sun.glass.ui.CommonDialogs;
import com.sun.glass.ui.EventLoop;
import com.sun.glass.ui.Launchable;
import com.sun.glass.ui.Screen;
import com.sun.glass.ui.Timer;
import com.sun.glass.ui.View;
import com.sun.glass.ui.Window;
import com.sun.javafx.embed.HostInterface;
import com.sun.javafx.geom.Path2D;
import com.sun.javafx.geom.PathIterator;
import com.sun.javafx.geom.Shape;
import com.sun.javafx.geom.transform.BaseTransform;
import com.sun.javafx.perf.PerformanceTracker;
import com.sun.javafx.runtime.async.AbstractRemoteResource;
import com.sun.javafx.runtime.async.AsyncOperationListener;
import com.sun.javafx.scene.paint.ImagePattern;
import com.sun.javafx.scene.text.HitInfo;
import com.sun.javafx.sg.PGArc;
import com.sun.javafx.sg.PGCircle;
import com.sun.javafx.sg.PGCubicCurve;
import com.sun.javafx.sg.PGEllipse;
import com.sun.javafx.sg.PGGroup;
import com.sun.javafx.sg.PGImageView;
import com.sun.javafx.sg.PGLine;
import com.sun.javafx.sg.PGMediaView;
import com.sun.javafx.sg.PGPath;
import com.sun.javafx.sg.PGPolygon;
import com.sun.javafx.sg.PGPolyline;
import com.sun.javafx.sg.PGQuadCurve;
import com.sun.javafx.sg.PGRectangle;
import com.sun.javafx.sg.PGRegion;
import com.sun.javafx.sg.PGSVGPath;
import com.sun.javafx.sg.PGShape.StrokeLineCap;
import com.sun.javafx.sg.PGShape.StrokeLineJoin;
import com.sun.javafx.sg.PGShape.StrokeType;
import com.sun.javafx.sg.PGText;
import com.sun.javafx.sg.prism.NGArc;
import com.sun.javafx.sg.prism.NGCircle;
import com.sun.javafx.sg.prism.NGCubicCurve;
import com.sun.javafx.sg.prism.NGEllipse;
import com.sun.javafx.sg.prism.NGGroup;
import com.sun.javafx.sg.prism.NGImageView;
import com.sun.javafx.sg.prism.NGLine;
import com.sun.javafx.sg.prism.NGMediaView;
import com.sun.javafx.sg.prism.NGNode;
import com.sun.javafx.sg.prism.NGPath;
import com.sun.javafx.sg.prism.NGPolygon;
import com.sun.javafx.sg.prism.NGPolyline;
import com.sun.javafx.sg.prism.NGQuadCurve;
import com.sun.javafx.sg.prism.NGRectangle;
import com.sun.javafx.sg.prism.NGRegion;
import com.sun.javafx.sg.prism.NGSVGPath;
import com.sun.javafx.sg.prism.NGText;
import com.sun.javafx.svgpath.parser.Path2DProducer;
import com.sun.javafx.tk.FileChooserType;
import com.sun.javafx.tk.FontLoader;
import com.sun.javafx.tk.ImageLoader;
import com.sun.javafx.tk.ScreenConfigurationAccessor;
import com.sun.javafx.tk.TKClipboard;
import com.sun.javafx.tk.TKDragGestureListener;
import com.sun.javafx.tk.TKDragSourceListener;
import com.sun.javafx.tk.TKDropTargetListener;
import com.sun.javafx.tk.TKScene;
import com.sun.javafx.tk.TKScreenConfigurationListener;
import com.sun.javafx.tk.TKStage;
import com.sun.javafx.tk.TKSystemMenu;
import com.sun.javafx.tk.Toolkit;
import com.sun.javafx.tk.desktop.AppletWindow;
import com.sun.javafx.tk.desktop.DesktopToolkit;
import com.sun.javafx.tk.desktop.MasterTimer;
import com.sun.prism.BasicStroke;
import com.sun.prism.ExternalImageTools;
import com.sun.prism.Graphics;
import com.sun.prism.GraphicsPipeline;
import com.sun.prism.PixelFormat;
import com.sun.prism.RTTexture;
import com.sun.prism.RenderingContext;
import com.sun.prism.ResourceFactory;
import com.sun.prism.ResourceFactoryListener;
import com.sun.prism.camera.PrismCameraImpl;
import com.sun.prism.camera.PrismParallelCameraImpl;
import com.sun.prism.camera.PrismPerspectiveCameraImpl;
import com.sun.prism.impl.PrismSettings;
import com.sun.prism.render.CompletionListener;
import com.sun.prism.render.RenderJob;
import com.sun.prism.render.ToolkitInterface;
import com.sun.scenario.DelayedRunnable;
import com.sun.scenario.animation.AbstractMasterTimer;
import com.sun.scenario.effect.FilterContext;
import com.sun.scenario.effect.Filterable;
import com.sun.scenario.effect.impl.prism.PrFilterContext;
import com.sun.scenario.effect.impl.prism.PrImage;
import com.sun.javafx.PlatformUtil;
import com.sun.prism.impl.Disposer;


public class QuantumToolkit extends DesktopToolkit implements Launchable, ToolkitInterface {

    private static boolean verbose = 
        AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
            @Override public Boolean run() {
                return Boolean.getBoolean("quantum.verbose");
            }
        });
    private static boolean debug = 
        AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
            @Override public Boolean run() {
                return Boolean.getBoolean("quantum.debug");
            }
        });
    private static Integer pulseHZ = 
        AccessController.doPrivileged(new PrivilegedAction<Integer>() {
            @Override public Integer run() {
                return Integer.getInteger("javafx.animation.pulse");
            }
        });
    private AtomicBoolean           toolkitRunning = new AtomicBoolean(false);
    private AtomicBoolean           animationRunning = new AtomicBoolean(false);
    private AtomicBoolean           nextPulseRequested = new AtomicBoolean(false);
    private AtomicBoolean           pulseRunning = new AtomicBoolean(false);
    private CountDownLatch          launchLatch = new CountDownLatch(1);
    
    protected final int             PULSE_INTERVAL = (int)(TimeUnit.SECONDS.toMillis(1L) / getRefreshRate());
    protected final int             FULLSPEED_INTERVAL = 1;     // ms
    private Runnable                pulseRunnable, userRunnable, timerRunnable;
    private Timer                   pulseTimer = null;
    private Thread                  shutdownHook = null;
    private PaintCollector          collector;
    private QuantumRenderer         renderer;
    private GraphicsPipeline        pipeline;

    private ClassLoader             ccl;

    private HashMap<Object,EventLoop> eventLoopMap = null;

    @Override public boolean init() {
        /*
         * AWT headful trips up glass, and is used in various places throughout
         * prism, such as J2DFontFactory and BufferedImageTools, but only
         * required AWT in a headless capacity.
         *
         * It is assumed that we don't need AWT headful, and if it is needed
         * then AWT is already initialized before this property is set.  In that
         * case the below is harmless.
         *
         * Further Consideration and Dependencies:
         * - This fix introduces a order dependency in Swing/AWT and FX interop
         * mode on Mac. Now Swing/AWT must be initialization before FX.
         *
         * - On Mac this causes the warning message "Process manager already
         * initialized -- can't fully enable headless mode."
         *
         * - On Windows AWT headful does not cause problems.
         *
         * - This Mac specific fix should be removed as soon as possible.
         * Once RT-12452 has been implemented, then we should be able to do
         * just that.  Introducing a long term restriction on Mac only, is worse
         * then adding universal platform restriction.
         */
        if (PlatformUtil.isMac()) {
            String version = AccessController.doPrivileged(new PrivilegedAction<String>() {
                @Override public String run() {
                    return System.getProperty("os.version");
                }
            });
            if (version.startsWith("10.4") || version.startsWith("10.5")) {
                throw new RuntimeException("JavaFX requires Mac OSX 10.6 or higher to run");
            }

            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                @Override public Void run() {
                    System.setProperty("java.awt.headless", "true");
                    return null;
                }
            });
        }
        /* 
         * Glass Mac, X11 need Application.setDeviceDetails to happen prior to Glass Application.Run
         */
        renderer = QuantumRenderer.getInstance();
        collector = PaintCollector.createInstance(this);
        pipeline = GraphicsPipeline.getPipeline();
        if (PrismSettings.shutdownHook) {
            shutdownHook = new Thread("Glass/Prism Shutdown Hook") {
                @Override public void run() {
                    dispose();
                }
            };
            Runtime.getRuntime().addShutdownHook(shutdownHook);
        }
        return true;
    }

    /**
     * This method is invoked by PlatformImpl. It is typically called on the main
     * thread, NOT the JavaFX Application Thread. The userStartupRunnable will
     * be invoked on the JavaFX Application Thread.
     *
     * @param userStartupRunnable A runnable invoked on the JavaFX Application Thread
     *                            that allows the system to perform some startup
     *                            functionality after the toolkit has been initialized.
     */
    @Override public void startup(final Runnable userStartupRunnable) {
        // Save the context class loader of the launcher thread
        ccl = Thread.currentThread().getContextClassLoader();

        try {
            this.userRunnable = userStartupRunnable;

            Application.Run(null, this);
        } catch (RuntimeException ex) {
            if (verbose) {
                ex.printStackTrace();
            }
            throw ex;
        } catch (Throwable t) {
            if (verbose) {
                t.printStackTrace();
            }
            throw new RuntimeException(t);
        }

        try {
            launchLatch.await();
        } catch (InterruptedException ie) {
            ie.printStackTrace();
        }
    }

    // restart the toolkit if previously terminated
    private void assertToolkitRunning() {
        /*
         * The restart path needs to be worked on - and synchronized 
         * such that the init() / startup() is not called multiple times
         */
        /* if (toolkitRunning.get() == false) {
            init();
            startup(null);
        }*/
    }

    @Override public void finishLaunching(String args[]) {
        Thread user = Thread.currentThread();

        if (toolkitRunning.getAndSet(true) == false) {
            user.setName("JavaFX Application Thread");

            // Set context class loader to the same as the thread that called startup
            user.setContextClassLoader(ccl);

            /* set uncaught handler for Glass native thread */
            user.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                @Override public void uncaughtException(Thread t, Throwable th) {
                    System.out.println(t.getName() + " uncaught: " + th.getClass().getName());
                    th.printStackTrace();
                }
            });
            setFxUserThread(user);

            /*
             *  Glass Application instance is now valid - create the ResourceFactory
             *  on the render thread
             */
            renderer.createResourceFactory();

            pulseRunnable = new Runnable() { 
                @Override public void run() {
                    QuantumToolkit.this.pulse();
                }
            };
            timerRunnable = new Runnable() { 
                @Override public void run() {
                    try {
                         QuantumToolkit.this.postPulse();
                    } catch (Throwable th) {
                        th.printStackTrace(System.err);
                        // } catch (RuntimeException re) {
                        // ignore spurious Glass timer events while exiting...
                    }
                }
            };
            pulseTimer = Application.GetApplication().createTimer(timerRunnable);

            Application.GetApplication().setEventHandler(new Application.EventHandler() {
                @Override public void handleQuitAction(Application app, long time) {
                    GlassStage.requestClosingAllWindows();
                }
            });
        }
        launchLatch.countDown();
        try {
            Application.invokeAndWait(this.userRunnable);
            
            if (getMasterTimer().isFullspeed()) {
                /* 
                 * FULLSPEED_INTVERVAL workaround
                 *
                 * Application.postOnEventQueue(pulseRunnable);
                 */
                pulseTimer.start(FULLSPEED_INTERVAL);
            } else {
                pulseTimer.start(PULSE_INTERVAL);
            }
        } catch (Throwable th) {
            th.printStackTrace(System.err);
        } finally {
            if (verbose) {
                System.err.println(" vsync: " + PrismSettings.isVsyncEnabled +
                                   " vpipe: " + pipeline.isVsyncSupported());
            }
            PerformanceTracker.logEvent("Toolkit.startup - finished");
        }
    }

    protected boolean isVsyncEnabled() {
        return (PrismSettings.isVsyncEnabled &&
                pipeline.isVsyncSupported() && 
                windowVisible(Window.getWindows()));
    }

    private boolean windowVisible(List<Window> windows) {
        if (windows.isEmpty()) {
            return false;
        } else {
            for (int i = 0; i < windows.size(); i++) {
                Window w = windows.get(i);
                if (w.isVisible() && w.isMinimized() == false) {
                    return true;
                }
            }
            return false;
        }
    }
    
    @Override public void checkFxUserThread() {
        super.checkFxUserThread();
        renderer.checkRendererIdle();
    }

    protected static Thread getFxUserThread() {
        return Toolkit.getFxUserThread();
    }
    
    /* com.sun.javafx.tk.ToolkitInterface */

    @Override public Future addRenderJob(RenderJob r) {
        return (renderer.submitRenderJob(r));
    }
   
    protected void postPulse() {
        if (toolkitRunning.get() &&
            (animationRunning.get() || nextPulseRequested.get() || collector.hasDirty().get()) &&
            !setPulseRunning()) {
            
            Application.postOnEventQueue(pulseRunnable);
            
            if (debug) {
                System.err.println("QT.postPulse@(" + System.nanoTime() + "): " + pulseString());
            }
        } else if (debug) {
            System.err.println("QT.postPulse#(" + System.nanoTime() + ") DROP: " + pulseString());
        }
    }
    
    private String pulseString() {
        return ((toolkitRunning.get() ? "T" : "t") +
                (animationRunning.get() ? "A" : "a") +
                (pulseRunning.get() ? "P" : "p") + 
                (nextPulseRequested.get() ? "N" : "n") +
                (collector.hasDirty().get() ? "D" : "d"));
    }
    
    private boolean setPulseRunning() {
        return (pulseRunning.getAndSet(true));
    }
    
    protected void endPulseRunning() {
        pulseRunning.set(false);
        if (debug) {
            System.err.println("QT.endPulse: " + System.nanoTime());
        }
    }
    
    protected void pulse() {
        if (debug) {
            System.err.println("PULSE:(" + System.nanoTime() + "): " + pulseString());
        }
        if (toolkitRunning.get() == false) {
            return;
        }
        try {
            nextPulseRequested.set(false);
            if (animationRunnable != null) {
                animationRunning.set(true);
                animationRunnable.run();
            } else {
                animationRunning.set(false);
            }
            firePulse();
            collector.renderAll();
        } catch (Throwable th) {
            th.printStackTrace(System.err);
        }
    }

    protected void vsyncHint() {
        if (isVsyncEnabled()) {
            if (debug) {
                System.err.println("QT.vsyncHint: postPulse: " + System.nanoTime());
            }
            postPulse();
        }
    }
    
    @Override  public AppletWindow createAppletWindow(long parent, String serverName) {
        GlassAppletWindow parentWindow = new GlassAppletWindow(parent, serverName);
        // Make this the parent window for all future Stages
        WindowStage.setAppletWindow(parentWindow);
        return parentWindow;
    }
    
    @Override public void closeAppletWindow() {
        GlassAppletWindow gaw = WindowStage.getAppletWindow();
        if (null != gaw) {
            gaw.dispose();
            WindowStage.setAppletWindow(null);
            // any further strong refs will be in the applet itself
        }
    }
    
    @Override public TKStage createTKStage(StageStyle stageStyle) {
        assertToolkitRunning();
        return new WindowStage(verbose, stageStyle).init(systemMenu);
    }

    @Override public TKStage createTKStage(StageStyle stageStyle, 
            boolean primary, Modality modality, TKStage owner) {
        assertToolkitRunning();
        return new WindowStage(verbose, stageStyle, primary, modality, owner).init(systemMenu);
    }

    @Override public Object enterNestedEventLoop(Object key) {
        checkFxUserThread();

        if (key == null) {
            throw new NullPointerException();
        }
        if (eventLoopMap == null) {
            eventLoopMap = new HashMap<Object, EventLoop>();
        }
        if (eventLoopMap.containsKey(key)) {
            throw new IllegalArgumentException(
                    "Key already associated with a running event loop: " + key);
        }
        EventLoop eventLoop = Application.GetApplication().createEventLoop();
        eventLoopMap.put(key, eventLoop);

        return eventLoop.enter();
    }

    @Override public void exitNestedEventLoop(Object key, Object rval) {
        checkFxUserThread();

        if (key == null) {
            throw new NullPointerException();
        }
        if (eventLoopMap == null || !eventLoopMap.containsKey(key)) {
            throw new IllegalArgumentException(
                    "Key not associated with a running event loop: " + key);
        }
        EventLoop eventLoop = eventLoopMap.get(key);
        eventLoopMap.remove(key);
        eventLoop.leave(rval);
    }

    @Override public TKStage createTKPopupStage(StageStyle stageStyle, Object owner) {
        assertToolkitRunning();
        return new PopupStage(verbose, owner).init(systemMenu);
    }

    @Override public TKStage createTKEmbeddedStage(HostInterface host) {
        assertToolkitRunning();
        return new EmbeddedStage(host);
    }

    private static ScreenConfigurationAccessor screenAccessor =
        new ScreenConfigurationAccessor() {
            @Override public int getMinX(Object obj) {
               return ((Screen)obj).getX();
            }
            @Override public int getMinY(Object obj) {
                return ((Screen)obj).getY();
            }
            @Override public int getWidth(Object obj) {
                return ((Screen)obj).getWidth();
            }
            @Override public int getHeight(Object obj) {
                return ((Screen)obj).getHeight();
            }
            @Override public int getVisualMinX(Object obj) {
                return ((Screen)obj).getVisibleX();
            }
            @Override public int getVisualMinY(Object obj) {
                return ((Screen)obj).getVisibleY();
            }
            @Override public int getVisualWidth(Object obj) {
                return ((Screen)obj).getVisibleWidth();
            }
            @Override public int getVisualHeight(Object obj) {
                return ((Screen)obj).getVisibleHeight();
            }
            @Override public float getDPI(Object obj) {
                return ((Screen)obj).getResolutionX();
            }
        };

    @Override public ScreenConfigurationAccessor 
                    setScreenConfigurationListener(final TKScreenConfigurationListener listener) {
        Screen.setCallback(new Screen.SettingsChangedCallback() {
            @Override public void settingsChanged() {
                notifyScreenListener(listener);
            }
        });
        return screenAccessor;
    }

    private static void notifyScreenListener(TKScreenConfigurationListener listener) {
        listener.screenConfigurationChanged();
    }

    @Override public Object getPrimaryScreen() {
        return Screen.getMainScreen();
    }

    @Override public List<?> getScreens() {
        return Screen.getScreens();
    }

    @Override public ImageLoader loadImage(String url, int width, int height, boolean preserveRatio, boolean smooth) {
        return new PrismImageLoader2(url, width, height, preserveRatio, smooth);
    }

    @Override public ImageLoader loadImage(InputStream stream, int width, int height,
                                           boolean preserveRatio, boolean smooth) {
        return new PrismImageLoader2(stream, width, height, preserveRatio, smooth);
    }

    @Override public AbstractRemoteResource<? extends ImageLoader> loadImageAsync(
            AsyncOperationListener listener, String url,
            int width, int height, boolean preserveRatio, boolean smooth) {
        return new PrismImageLoader2.AsyncImageLoader(listener, url, width, height, preserveRatio, smooth);
    }

    @Override public void defer(Runnable runnable) {
        if (!toolkitRunning.get()) {
            throw new IllegalStateException("Attempt to call defer when toolkit not running");
        }
        Application.invokeLater(runnable);
    }

    @Override public void exit() {

        notifyShutdownHooks();
        pulseTimer.stop();
        dispose();

        // TODO: Applet
        Application app = Application.GetApplication();
        app.terminate();
        
        super.exit();
    }

    public void dispose() {
        if (toolkitRunning.compareAndSet(true, false)) {
            renderer.stopRenderer();

            if (PrismSettings.shutdownHook) {
                try {
                    Runtime.getRuntime().removeShutdownHook(shutdownHook);
                } catch (IllegalStateException ignore) {
                    // throw when shutdown hook already removed
                }
            }
        }
    }

    @Override public boolean isForwardTraversalKey(KeyEvent e) {
        return (e.getCode() == KeyCode.TAB)
                   && (e.getEventType() == KeyEvent.KEY_PRESSED)
                   && !e.isShiftDown();
    }

    @Override public boolean isBackwardTraversalKey(KeyEvent e) {
        return (e.getCode() == KeyCode.TAB)
                   && (e.getEventType() == KeyEvent.KEY_PRESSED)
                   && e.isShiftDown();
    }

    private Map contextMap = Collections.synchronizedMap(new HashMap());
    @Override public Map<Object, Object> getContextMap() {
        return contextMap;
    }

    @Override public int getRefreshRate() {
        if (pulseHZ == null) {
            return 60;
        } else {
            return pulseHZ.intValue();
        }
    }

    private DelayedRunnable animationRunnable;
    @Override public void setAnimationRunnable(DelayedRunnable animationRunnable) {
        if (animationRunnable != null) {
            animationRunning.set(true);
        }
        this.animationRunnable = animationRunnable;
    }

    @Override public void requestNextPulse() {
        nextPulseRequested.set(true);
    }
    
    @Override public void waitFor(Task t) {
        if (t.isFinished()) {
            return;
        }
    }

    @Override public PrismPerspectiveCameraImpl createPerspectiveCamera() {
        return new PrismPerspectiveCameraImpl();
    }

    @Override public PrismParallelCameraImpl createParallelCamera() {
        return PrismParallelCameraImpl.getInstance();
    }

    @Override protected Object createColorPaint(Color color) {
        return new com.sun.prism.paint.Color(
                (float)color.getRed(), (float)color.getGreen(),
                (float)color.getBlue(), (float)color.getOpacity());
    }

    private com.sun.prism.paint.Color toPrismColor(Color color) {
        return (com.sun.prism.paint.Color) color.impl_getPlatformPaint();
    }

    private List<com.sun.prism.paint.Stop> convertStops(List<Stop> paintStops) {
        List<com.sun.prism.paint.Stop> stops =
            new ArrayList<com.sun.prism.paint.Stop>(paintStops.size());
        for (Stop s : paintStops) {
            stops.add(new com.sun.prism.paint.Stop(toPrismColor(s.getColor()),
                                                   (float) s.getOffset()));
        }
        return stops;
    }

    @Override protected Object createLinearGradientPaint(LinearGradient paint) {
        int cmi = com.sun.prism.paint.Gradient.REPEAT;
        CycleMethod cycleMethod = paint.getCycleMethod();
        if (cycleMethod == CycleMethod.NO_CYCLE) {
            cmi = com.sun.prism.paint.Gradient.PAD;
        } else if (cycleMethod == CycleMethod.REFLECT) {
            cmi = com.sun.prism.paint.Gradient.REFLECT;
        }
        // TODO: extract colors/offsets and pass them in directly...
        List<com.sun.prism.paint.Stop> stops = convertStops(paint.getStops());
        return new com.sun.prism.paint.LinearGradient(
            (float)paint.getStartX(), (float)paint.getStartY(), (float)paint.getEndX(), (float)paint.getEndY(),
            null, paint.isProportional(), cmi, stops);
    }

    @Override
    protected Object createRadialGradientPaint(RadialGradient paint) {
        float cx = (float)paint.getCenterX();
        float cy = (float)paint.getCenterY();
        float fa = (float)paint.getFocusAngle();
        float fd = (float)paint.getFocusDistance();

        int cmi = 0;
        if (paint.getCycleMethod() == CycleMethod.NO_CYCLE) {
            cmi = com.sun.prism.paint.Gradient.PAD;
        } else if (paint.getCycleMethod() == CycleMethod.REFLECT) {
            cmi = com.sun.prism.paint.Gradient.REFLECT;
        } else {
            cmi = com.sun.prism.paint.Gradient.REPEAT;
        }

        // TODO: extract colors/offsets and pass them in directly...
        List<com.sun.prism.paint.Stop> stops = convertStops(paint.getStops());
        return new com.sun.prism.paint.RadialGradient(cx, cy, fa, fd,
                (float)paint.getRadius(), null, paint.isProportional(), cmi, stops);
    }

    @Override
    protected Object createImagePatternPaint(ImagePattern paint) {
        if (paint.getImage() == null) {
            return com.sun.prism.paint.Color.TRANSPARENT;
        } else {
            return new com.sun.prism.paint.ImagePattern((com.sun.prism.Image) paint.getImage().impl_getPlatformImage(),
                    paint.getX(),
                    paint.getY(),
                    paint.getWidth(),
                    paint.getHeight(),
                    paint.isProportional());
        }
    }

    static BasicStroke tmpStroke = new BasicStroke();
    private void initStroke(StrokeType pgtype, double strokewidth,
                            StrokeLineCap pgcap,
                            StrokeLineJoin pgjoin, float miterLimit,
                            float[] dashArray, float dashOffset)
    {
        int type;
        if (pgtype == StrokeType.CENTERED) {
            type = BasicStroke.TYPE_CENTERED;
        } else if (pgtype == StrokeType.INSIDE) {
            type = BasicStroke.TYPE_INNER;
        } else {
            type = BasicStroke.TYPE_OUTER;
        }

        int cap;
        if (pgcap == StrokeLineCap.BUTT) {
            cap = BasicStroke.CAP_BUTT;
        } else if (pgcap == StrokeLineCap.SQUARE) {
            cap = BasicStroke.CAP_SQUARE;
        } else {
            cap = BasicStroke.CAP_ROUND;
        }

        int join;
        if (pgjoin == StrokeLineJoin.BEVEL) {
            join = BasicStroke.JOIN_BEVEL;
        } else if (pgjoin == StrokeLineJoin.MITER) {
            join = BasicStroke.JOIN_MITER;
        } else {
            join = BasicStroke.JOIN_ROUND;
        }

        tmpStroke.set(type, (float) strokewidth, cap, join, miterLimit);
        if ((dashArray != null) && (dashArray.length > 0)) {
            tmpStroke.set(dashArray, dashOffset);
        } else {
            tmpStroke.set(null, 0);
        }
    }

    @Override
    public void accumulateStrokeBounds(Shape shape, float bbox[],
                                       StrokeType pgtype,
                                       double strokewidth,
                                       StrokeLineCap pgcap,
                                       StrokeLineJoin pgjoin,
                                       float miterLimit,
                                       BaseTransform tx)
    {

        initStroke(pgtype, strokewidth, pgcap, pgjoin, miterLimit, null, 0);
        if (tx.isTranslateOrIdentity()) {
            tmpStroke.accumulateShapeBounds(bbox, shape, tx);
        } else {
            Shape.accumulate(bbox, tmpStroke.createStrokedShape(shape), tx);
        }
    }

    @Override
    public boolean strokeContains(Shape shape, double x, double y,
                                  StrokeType pgtype,
                                  double strokewidth,
                                  StrokeLineCap pgcap,
                                  StrokeLineJoin pgjoin,
                                  float miterLimit)
    {
        initStroke(pgtype, strokewidth, pgcap, pgjoin, miterLimit, null, 0);
        // TODO: The contains testing could be done directly without creating a Shape
        return tmpStroke.createStrokedShape(shape).contains((float) x, (float) y);
    }

    @Override
    public Shape createStrokedShape(Shape shape,
                                    StrokeType pgtype,
                                    double strokewidth,
                                    StrokeLineCap pgcap,
                                    StrokeLineJoin pgjoin,
                                    float miterLimit,
                                    float[] dashArray,
                                    float dashOffset) {
        initStroke(pgtype, strokewidth, pgcap, pgjoin, miterLimit,
                   dashArray, dashOffset);
        return tmpStroke.createStrokedShape(shape);
    }

    @Override public Dimension2D getBestCursorSize(int preferredWidth, int preferredHeight) {
        return CursorUtils.getBestCursorSize(preferredWidth, preferredHeight);
    }

    @Override public int getMaximumCursorColors() {
        return 2;
    }

    @Override public int getKeyCodeForChar(String character) {
        // TODO: shouldn't we support String here?
        return (character.length() == 1)
                ? com.sun.glass.events.KeyEvent.getKeyCodeForChar(
                          character.charAt(0))
                : com.sun.glass.events.KeyEvent.VK_UNDEFINED;
    }

    @Override public javafx.scene.input.MouseEvent convertMouseEventToFX(Object event) {
        if (event instanceof GlassPrismMouseEvent) {
            return PrismEventUtils.glassMouseEventToFX((GlassPrismMouseEvent)event);
        } else if (event instanceof MouseEvent) {
            return (MouseEvent)event;
        }
        return null;
    }

    @Override public javafx.scene.input.KeyEvent convertKeyEventToFX(Object event) {
        if (event instanceof GlassPrismKeyEvent) {
            return PrismEventUtils.glassKeyEventToFX((GlassPrismKeyEvent)event);
        } else if (event instanceof KeyEvent) {
            return (KeyEvent)event;
        }
        return null;
    }

    @Override public javafx.scene.input.InputMethodEvent convertInputMethodEventToFX(Object event) {
        if (event instanceof GlassPrismInputMethodEvent) {
            return PrismEventUtils.glassInputMethodEventToFX((GlassPrismInputMethodEvent)event);
        } else if (event instanceof InputMethodEvent) {
            return (InputMethodEvent)event;
        }

        return null;
    }

    @Override public PathElement[] convertShapeToFXPath(Object shape) {
        if (shape == null) {
            return new PathElement[0];
        }
        List<PathElement> elements = new ArrayList();
        // iterate over the shape and turn it into a series of path
        // elements
        com.sun.javafx.geom.Shape geomShape = (com.sun.javafx.geom.Shape) shape;
        PathIterator itr = geomShape.getPathIterator(null);
        PathIteratorHelper helper = new PathIteratorHelper(itr);
        PathIteratorHelper.Struct struct = new PathIteratorHelper.Struct();

        while (!helper.isDone()) {
            // true if WIND_EVEN_ODD, false if WIND_NON_ZERO
            // TODO Apparently winding rules are not exposed in FX yet??
            boolean windEvenOdd = helper.getWindingRule() == PathIterator.WIND_EVEN_ODD;
            int type = helper.currentSegment(struct);
            PathElement el;
            if (type == PathIterator.SEG_MOVETO) {
                el = new MoveTo(struct.f0, struct.f1);
            } else if (type == PathIterator.SEG_LINETO) {
                el = new LineTo(struct.f0, struct.f1);
            } else if (type == PathIterator.SEG_QUADTO) {
                el = new QuadCurveTo(
                    struct.f0,
                    struct.f1,
                    struct.f2,
                    struct.f3);
            } else if (type == PathIterator.SEG_CUBICTO) {
                el = new CubicCurveTo (
                    struct.f0,
                    struct.f1,
                    struct.f2,
                    struct.f3,
                    struct.f4,
                    struct.f5);
            } else if (type == PathIterator.SEG_CLOSE) {
                el = new ClosePath();
            } else {
                throw new IllegalStateException("Invalid element type: " + type);
            }
            helper.next();
            elements.add(el);
        }

        return elements.toArray(new PathElement[elements.size()]);
    }

    @Override public HitInfo convertHitInfoToFX(Object hit) {
        Integer textHitPos = (Integer) hit;
        HitInfo hitInfo = new HitInfo();
        hitInfo.setCharIndex(textHitPos.intValue());
        hitInfo.setLeading(true);
        return hitInfo;
    }

    @Override public Filterable toFilterable(Image img) {
        return PrImage.create((com.sun.prism.Image) img.impl_getPlatformImage());
    }

    @Override public FilterContext getFilterContext(Object config) {
        if (config == null || ((config instanceof com.sun.glass.ui.Screen) == false)) {
            return PrFilterContext.getDefaultInstance();
        }
        Screen screen = (Screen)config;
        return PrFilterContext.getInstance(screen);
    }

    @Override public AbstractMasterTimer getMasterTimer() {
        return MasterTimer.getInstance();
    }

    @Override public FontLoader getFontLoader() {
        return com.sun.javafx.font.PrismFontLoader.getInstance();
    }

    @Override public PGArc createPGArc() {
        return new NGArc();
    }

    @Override public PGCircle createPGCircle() {
        return new NGCircle();
    }

    @Override public PGCubicCurve createPGCubicCurve() {
        return new NGCubicCurve();
    }

    @Override public PGEllipse createPGEllipse() {
        return new NGEllipse();
    }

    @Override public PGLine createPGLine() {
        return new NGLine();
    }

    @Override public PGPath createPGPath() {
        return new NGPath();
    }

    @Override public PGSVGPath createPGSVGPath() {
        return new NGSVGPath();
    }

    @Override public PGPolygon createPGPolygon() {
        return new NGPolygon();
    }

    @Override public PGPolyline createPGPolyline() {
        return new NGPolyline();
    }

    @Override public PGQuadCurve createPGQuadCurve() {
        return new NGQuadCurve();
    }

    @Override public PGRectangle createPGRectangle() {
        return new NGRectangle();
    }

    @Override public PGImageView createPGImageView() {
        return new NGImageView();
    }

    @Override public PGMediaView createPGMediaView() {
        return new NGMediaView();
    }

    @Override public PGGroup createPGGroup() {
        return new NGGroup();
    }

    @Override public PGText createPGText() {
        return new NGText();
    }

    @Override public PGRegion createPGRegion() {
        return new NGRegion();
    }

    @Override public Object createSVGPathObject(SVGPath svgpath) {
        try {
            return Path2DProducer.createPath(new StringReader(svgpath.getContent()),
                svgpath.getFillRule());
        } catch (IOException e) {}
        return null;
    }

    @Override public Path2D createSVGPath2D(SVGPath svgpath) {
        try {
            return Path2DProducer.createPath(new StringReader(svgpath.getContent()),
                svgpath.getFillRule());
        } catch (IOException e) {}
        return null;
    }

    @Override public boolean imageContains(Object image, float x, float y) {
        if (image == null) {
            return false;
        }

        com.sun.prism.Image pImage = (com.sun.prism.Image)image;
        int intX = (int)x + pImage.getMinX();
        int intY = (int)y + pImage.getMinY();

        if (pImage.getPixelFormat().isOpaque()) {
            return true;
        }

        if (pImage.getPixelFormat() == PixelFormat.INT_ARGB_PRE) {
            IntBuffer ib = (IntBuffer) pImage.getPixelBuffer();
            int index = intX + intY * pImage.getRowLength();
            if (index >= ib.limit()) {
                return false;
            } else {
                return (ib.get(index) & 0xff000000) != 0;
            }
        } else if (pImage.getPixelFormat() == PixelFormat.BYTE_BGRA_PRE) {
            ByteBuffer bb = (ByteBuffer) pImage.getPixelBuffer();
            int index = intX * pImage.getBytesPerPixelUnit() + intY * pImage.getScanlineStride() + 3;
            if (index >= bb.limit()) {
                return false;
            } else {
                return (bb.get(index) & 0xff) != 0;
            }
        } else if (pImage.getPixelFormat() == PixelFormat.BYTE_ALPHA) {
            ByteBuffer bb = (ByteBuffer) pImage.getPixelBuffer();
            int index = intX * pImage.getBytesPerPixelUnit() + intY * pImage.getScanlineStride();
            if (index >= bb.limit()) {
                return false;
            } else {
                return (bb.get(index) & 0xff) != 0;
            }
        }
        return true;
    }

    @Override
    public boolean isSupported(ConditionalFeature feature) {
        if (feature == ConditionalFeature.SCENE3D) {
            return GraphicsPipeline.getPipeline().is3DSupported();
        } else if (feature == ConditionalFeature.EFFECT) {
            return GraphicsPipeline.getPipeline().isEffectSupported();
        } else if (feature == ConditionalFeature.SHAPE_CLIP) {
            return true;
        } else if (feature == ConditionalFeature.INPUT_METHOD) {
            return false;     // Glass TODO: input methods
        } else if (feature == ConditionalFeature.TRANSPARENT_WINDOW) {
            return Application.GetApplication().supportsTransparentWindows();
        } else {
            return false;
        }
    }

    private QuantumClipboard clipboard;
    @Override public TKClipboard getSystemClipboard() {
        if (clipboard == null) {
            clipboard = QuantumClipboard.getClipboardInstance(new ClipboardAssistance(com.sun.glass.ui.Clipboard.SYSTEM));
        }
        return clipboard;
    }

    private GlassSystemMenu systemMenu = new GlassSystemMenu();
    @Override public TKSystemMenu getSystemMenu() {
        return systemMenu;
    }

    // TODO need to implement, probably need Glass support
    @Override public TKClipboard getNamedClipboard(String name) {
        return null;
    }

    @Override public DragEvent convertDragRecognizedEventToFX(Object event, Dragboard dragboard) {
        return PrismEventUtils.glassDragGestureToFX((GlassDragEvent)event, dragboard);
    }

    @Override public DragEvent convertDragSourceEventToFX(Object event, Dragboard dragboard) {
        return PrismEventUtils.glassDragSourceEventToFX((GlassDragEvent)event, dragboard);
    }

    @Override public DragEvent convertDropTargetEventToFX(Object event, Dragboard dragboard) {
        return PrismEventUtils.glassDropTargetEventToFX((GlassDragEvent)event, dragboard);
    }

    private QuantumClipboard dragSourceClipboard;
    private GlassScene dragSourceScene;

    @Override public Dragboard createDragboard() {
        QuantumClipboard peer = QuantumClipboard.getDragboardInstance(new ClipboardAssistance(com.sun.glass.ui.Clipboard.DND) {
            @Override public void actionPerformed(int performedAction) {
                if (dragSourceClipboard != null && dragSourceScene != null && dragSourceScene.dragSourceListener != null) {
                    Dragboard dragboard = Dragboard.impl_create(dragSourceClipboard);
                    GlassDragEvent event = new GlassDragEvent(GlassDragEvent.END, 0, 0, 0, 0, dragboard, 0, dragSourceScene,
                            performedAction);
                    dragSourceScene.dragSourceListener.dragDropEnd(event);
                }
                dragSourceClipboard = null;
                dragSourceScene = null;
            }
        });
        return Dragboard.impl_create(peer);
    }

    @Override public void startDrag(Object o, Set<TransferMode> tm, TKDragSourceListener l, Dragboard dragboard) {
        if (dragboard == null) {
            throw new IllegalArgumentException("dragboard should not be null");
        }

        GlassScene view;
        if (o instanceof GlassDragEvent) {
            GlassDragEvent gde = (GlassDragEvent)o;
            view = gde.getGlassScene();
        } else if (o instanceof GlassScene) {
            view = (GlassScene)o;
        } else {
            throw new IllegalArgumentException("o should be either a GlassScene, or a GlassDragEvent instance");
        }
        view.setTKDragSourceListener(l);

        QuantumClipboard gc = (QuantumClipboard)dragboard.impl_getPeer();

        dragSourceClipboard = gc;
        dragSourceScene = view;

        gc.setSupportedTransferMode(tm);
        gc.flush();

        // we're back from the modal DnD event loop
        gc.close();
    }

    @Override public void enableDrop(TKScene s, TKDropTargetListener l) {

        assert s instanceof GlassScene;

        GlassScene view = (GlassScene)s;
        view.setTKDropTargetListener(l);
    }

    @Override public void registerDragGestureListener(TKScene s, Set<TransferMode> tm, TKDragGestureListener l) {

        assert s instanceof GlassScene;

        GlassScene view = (GlassScene)s;
        view.setTKDragGestureListener(l);
    }

    /* com.sun.javafx.tk.desktop.DesktopToolkit */
    @Override public boolean isAppletDragSupported() {
        // TODO Glass Applet hooks
        return false;
    }

    @Override
    public void installInputMethodRequests(TKScene scene, InputMethodRequests requests) {

        assert scene instanceof GlassScene;

        GlassScene view = (GlassScene)scene;
        view.setInputMethodRequests(requests);
    }
    
    static class PlatformImage implements com.sun.javafx.tk.ImageLoader, ResourceFactoryListener {

        // cache rt here
        private com.sun.prism.RTTexture rt;
        private com.sun.prism.Image image;
        private ResourceFactory rf;
        
        PlatformImage(com.sun.prism.Image image) { 
            this.image = image;
        }
        
        RTTexture getRT(int w, int h, ResourceFactory rfNew) {
            boolean rttOk = rt != null && rf == rfNew && 
                    rt.getContentWidth() == w && rt.getContentHeight() == h;
            
            if (!rttOk) {
                if (rt != null) {
                    rt.dispose();
                }
                if (rf != null) {
                    rf.removeFactoryListener(this);
                    rf = null;
                }
                rt = rfNew.createRTTexture(w, h);
                if (rt != null) {
                    rf = rfNew;
                    rf.addFactoryListener(this);
                }
            }
            
            return rt;
        }
        
        void dispose() {
            if (rt != null) {
                rt.dispose();
                rt = null;
            }
        }
        
        void setImage(com.sun.prism.Image img) {
            image = img;
        }
        
        @Override
        public boolean getError() { return image == null; }
        @Override
        public int getFrameCount() { return 1; }
        @Override
        public Object[] getFrames() { return new Object[] { image }; }
        @Override
        public Object getFrame(int index) { return image; }
        @Override
        public int getFrameDelay(int index) { return 0; }
        @Override
        public int getWidth() { return image.getWidth(); }
        @Override
        public int getHeight() { return image.getHeight(); }
        @Override 
        public void factoryReset() { dispose(); }
        @Override 
        public void factoryReleased() { dispose(); }
    }

    
    @Override
    public boolean isExternalFormatSupported(Class format) {
        return ExternalImageTools.isFormatSupported(format);
    }

    @Override public ImageLoader loadPlatformImage(Object platformImage) {
        if (platformImage instanceof PlatformImage) {
            return (PlatformImage)platformImage;
        }
        
        if (platformImage instanceof com.sun.prism.Image) {
            return new PlatformImage((com.sun.prism.Image) platformImage);
        }
        
        ExternalImageTools.IImporter imp = ExternalImageTools.getImporter(platformImage);
        
        if (imp != null) {
            return new PlatformImage(imp.loadExternalImage(platformImage));
        }
        
        throw new UnsupportedOperationException("unsupported class for loadPlatformImage");
    }
    
    @Override
    public Object renderToImage(ImageRenderingContext p) {
        Object saveImage = p.platformImage;
        final ImageRenderingContext params = p;
        final com.sun.prism.paint.Paint currentPaint = p.platformPaint instanceof com.sun.prism.paint.Paint ? 
                (com.sun.prism.paint.Paint)p.platformPaint : null;
        
        RenderJob re = new RenderJob(new Runnable() {

            private com.sun.prism.paint.Color getClearColor() {
                if (currentPaint == null) {
                    return com.sun.prism.paint.Color.WHITE;
                } else if (currentPaint.getType() == com.sun.prism.paint.Paint.Type.COLOR) {
                    return (com.sun.prism.paint.Color) currentPaint;
                } else if (currentPaint.isOpaque()) {
                    return com.sun.prism.paint.Color.TRANSPARENT;
                } else {
                    return com.sun.prism.paint.Color.WHITE;
                }
            }

            void draw(com.sun.prism.Graphics g, int w, int h) {
                g.setDepthBuffer(params.depthBuffer);

                g.clear(getClearColor());
                if (currentPaint != null && 
                        currentPaint.getType() != com.sun.prism.paint.Paint.Type.COLOR) {
                    g.getRenderTarget().setOpaque(currentPaint.isOpaque());
                    g.setPaint(currentPaint);
                    g.fillQuad(0, 0, w, h);
                }

                if (params.scale != 1.0) {
                    g.scale((float)params.scale, (float)params.scale);
                }
                
                if (params.root != null) {
                    if (params.camera instanceof PrismCameraImpl) {
                        g.setCamera((PrismCameraImpl)params.camera);
                    }
                    
                    NGNode ngNode = (NGNode)params.root;
                    ngNode.render(g);
                }

            }
            
            @Override
            public void run() {

                ResourceFactory rf = GraphicsPipeline.getDefaultResourceFactory();

                if (!rf.isDeviceReady()) {
                    return;
                }
                
                int w = (int)Math.ceil(params.width*params.scale);
                int h = (int)Math.ceil(params.height*params.scale);

                if (w <= 0 || h <= 0) {
                    return;
                }

                RenderingContext context = rf.createRenderingContext(null);
                try {
                    
                    context.begin();

                    PlatformImage pImage = (params.platformImage instanceof PlatformImage) ?
                            (PlatformImage)params.platformImage : new PlatformImage(null);
                    
                    com.sun.prism.RTTexture rt = pImage.getRT(w, h, rf);
                    
                    if (rt == null) {
                        return;
                    }
                    
                    Graphics g = rt.createGraphics();

                    draw(g, w, h);

                    int[] pixels = pImage.rt.getPixels();
                    
                    if (pixels != null) {
                        pImage.setImage(com.sun.prism.Image.fromIntArgbPreData(pixels, w, h));
                    } else {
                        IntBuffer ib = IntBuffer.allocate(w*h);
                        if (pImage.rt.readPixels(ib)) {
                            pImage.setImage(com.sun.prism.Image.fromIntArgbPreData(ib, w, h));
                        } else {
                            pImage.dispose();
                            pImage = null;
                        }
                    }

                    params.platformImage = pImage;

                } catch (Throwable t) {
                    t.printStackTrace(System.err);
                } finally {
                    Disposer.cleanUp();
                    context.end();
                }
            }
        });

        final CountDownLatch latch = new CountDownLatch(1);
        re.setCompletionListener(new CompletionListener() {
            @Override public void done(final RenderJob job) {
                latch.countDown();
            }
        });
        Future job = addRenderJob(re);

        do {
            try {
                latch.await();
                break;
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        } while (true);

        Object image = params.platformImage;
        params.platformImage = saveImage;

        return image;
    }

    @Override
    public Object toExternalImage(Object pImg, Object imgType) {
        boolean isPrismImage = pImg instanceof com.sun.prism.Image;

        ExternalImageTools.IExporter exp = isPrismImage && imgType != null ? 
                ExternalImageTools.getExporter(imgType) : null;

        return exp != null 
                ? exp.exportPrismImage((com.sun.prism.Image)pImg, imgType) : null;
    }

    @Override
    public List<File> showFileChooser(final TKStage ownerWindow,
                                      final String title,
                                      final File initialDirectory,
                                      final FileChooserType fileChooserType,
                                      final List<FileChooser.ExtensionFilter>
                                              extensionFilters) {
        WindowStage blockedStage = null;
        try {
            // NOTE: we block the owner of the owner deliberately.
            //       The native system blocks the nearest owner itself.
            //       Otherwise sheets on Mac are unusable.
            blockedStage = blockOwnerStage(ownerWindow);
            
            return CommonDialogs.showFileChooser(
                    (ownerWindow instanceof WindowStage)
                            ? ((WindowStage) ownerWindow).getPlatformWindow()
                            : null,
                    initialDirectory,
                    title,
                    (fileChooserType == FileChooserType.SAVE)
                            ? CommonDialogs.Type.SAVE
                            : CommonDialogs.Type.OPEN,
                    (fileChooserType == FileChooserType.OPEN_MULTIPLE),
                    convertExtensionFilters(extensionFilters));
        } finally {
            if (blockedStage != null) {
                blockedStage.setEnabled(true);
            }
        }
    }

    @Override
    public File showDirectoryChooser(final TKStage ownerWindow,
                                     final String title,
                                     final File initialDirectory) {
        WindowStage blockedStage = null;
        try {
            // NOTE: we block the owner of the owner deliberately.
            //       The native system blocks the nearest owner itself.
            //       Otherwise sheets on Mac are unusable.
            blockedStage = blockOwnerStage(ownerWindow);

            return CommonDialogs.showFolderChooser(
                    (ownerWindow instanceof WindowStage)
                            ? ((WindowStage) ownerWindow).getPlatformWindow()
                            : null,
                    initialDirectory, title);
        } finally {
            if (blockedStage != null) {
                blockedStage.setEnabled(true);
            }
        }
    }

    private WindowStage blockOwnerStage(final TKStage stage) {
        if (stage instanceof WindowStage) {
            final TKStage ownerStage = ((WindowStage) stage).getOwner();
            if (ownerStage instanceof WindowStage) {
                final WindowStage ownerWindowStage = (WindowStage) ownerStage;
                ownerWindowStage.setEnabled(false);
                return ownerWindowStage;
            }
        }

        return null;
    }

    private static List<CommonDialogs.ExtensionFilter>
            convertExtensionFilters(final List<FileChooser.ExtensionFilter>
                                            extensionFilters) {
        final CommonDialogs.ExtensionFilter[] glassExtensionFilters =
                new CommonDialogs.ExtensionFilter[extensionFilters.size()];

        int i = 0;
        for (final FileChooser.ExtensionFilter extensionFilter:
                 extensionFilters) {
            glassExtensionFilters[i++] =
                    new CommonDialogs.ExtensionFilter(
                            extensionFilter.getDescription(),
                            extensionFilter.getExtensions());
        }

        return Arrays.asList(glassExtensionFilters);
    }

    @Override
    public long getMultiClickTime() {
        return View.getMultiClickTime();
    }

    @Override
    public int getMultiClickMaxX() {
        return View.getMultiClickMaxX();
    }   

    @Override
    public int getMultiClickMaxY() {
        return View.getMultiClickMaxY();
    }
}
