Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene;
027
028import java.util.HashMap;
029import java.util.Map;
030
031import javafx.beans.InvalidationListener;
032import javafx.beans.Observable;
033import javafx.beans.property.ReadOnlyDoubleProperty;
034import javafx.beans.property.ReadOnlyDoublePropertyBase;
035import javafx.beans.property.ReadOnlyObjectProperty;
036import javafx.beans.property.ReadOnlyObjectPropertyBase;
037import javafx.geometry.Dimension2D;
038import javafx.scene.image.Image;
039
040import com.sun.javafx.cursor.CursorFrame;
041import com.sun.javafx.cursor.ImageCursorFrame;
042import com.sun.javafx.tk.Toolkit;
043import java.util.Arrays;
044
045
046/**
047 * A custom image representation of the mouse cursor. On platforms that don't
048 * support custom cursors, {@code Cursor.DEFAULT} will be used in place of the
049 * specified ImageCursor.
050 *
051 * <p>Example:
052 * <pre>
053import javafx.scene.*;
054import javafx.scene.image.*;
055
056Image image = new Image("mycursor.png");
057
058Scene scene = new Scene(400, 300);
059scene.setCursor(new ImageCursor(image,
060                                image.getWidth() / 2,
061                                image.getHeight() /2));
062 * </pre>
063 *
064 * @since JavaFX 1.3
065 */
066public class ImageCursor extends Cursor {
067    /**
068     * The image to display when the cursor is active. If the image is null,
069     * {@code Cursor.DEFAULT} will be used.
070     *
071     * @defaultValue null
072     */
073    private ObjectPropertyImpl<Image> image;
074
075    public final Image getImage() {
076        return image == null ? null : image.get();
077    }
078
079    public final ReadOnlyObjectProperty<Image> imageProperty() {
080        return imagePropertyImpl();
081    }
082
083    private ObjectPropertyImpl<Image> imagePropertyImpl() {
084        if (image == null) {
085            image = new ObjectPropertyImpl<Image>("image");
086        }
087
088        return image;
089    }
090
091    /**
092     * The X coordinate of the cursor's hot spot. This hotspot represents the
093     * location within the cursor image that will be displayed at the mouse
094     * position. This must be in the range of [0,image.width-1]. A value
095     * less than 0 will be set to 0. A value greater than
096     * image.width-1 will be set to image.width-1.
097     *
098     * @defaultValue 0
099     */
100    private DoublePropertyImpl hotspotX;
101
102    public final double getHotspotX() {
103        return hotspotX == null ? 0.0 : hotspotX.get();
104    }
105
106    public final ReadOnlyDoubleProperty hotspotXProperty() {
107        return hotspotXPropertyImpl();
108    }
109
110    private DoublePropertyImpl hotspotXPropertyImpl() {
111        if (hotspotX == null) {
112            hotspotX = new DoublePropertyImpl("hotspotX");
113        }
114
115        return hotspotX;
116    }
117
118    /**
119     * The Y coordinate of the cursor's hot spot. This hotspot represents the
120     * location within the cursor image that will be displayed at the mouse
121     * position. This must be in the range of [0,image.height-1]. A value
122     * less than 0 will be set to 0. A value greater than
123     * image.height-1 will be set to image.height-1.
124     *
125     * @defaultValue 0
126     */
127    private DoublePropertyImpl hotspotY;
128
129    public final double getHotspotY() {
130        return hotspotY == null ? 0.0 : hotspotY.get();
131    }
132
133    public final ReadOnlyDoubleProperty hotspotYProperty() {
134        return hotspotYPropertyImpl();
135    }
136
137    private DoublePropertyImpl hotspotYPropertyImpl() {
138        if (hotspotY == null) {
139            hotspotY = new DoublePropertyImpl("hotspotY");
140        }
141
142        return hotspotY;
143    }
144
145    private CursorFrame currentCursorFrame;
146
147    /**
148     * Stores the first cursor frame. For non-animated cursors there is only one
149     * frame and so the {@code restCursorFrames} is {@code null}.
150     */
151    private ImageCursorFrame firstCursorFrame;
152
153    /**
154     * Maps platform images to cursor frames. It doesn't store the first cursor
155     * frame and so it needs to be created only for animated cursors.
156     */
157    private Map<Object, ImageCursorFrame> otherCursorFrames;
158
159    /**
160     * Indicates whether the image cursor is currently in use. The active cursor
161     * is bound to the image and invalidates its platform cursor when the image
162     * changes.
163     */
164    private int activeCounter;
165
166    /**
167     * Constructs a new empty {@code ImageCursor} which will look as
168     * {@code Cursor.DEFAULT}.
169     */
170    public ImageCursor() {
171    }
172
173    /**
174     * Constructs an {@code ImageCursor} from the specified image. The cursor's
175     * hot spot will default to the upper left corner.
176     *
177     * @param image the image
178     */
179    public ImageCursor(final Image image) {
180        this(image, 0f, 0f);
181    }
182
183    /**
184     * Constructs an {@code ImageCursor} from the specified image and hotspot
185     * coordinates.
186     *
187     * @param image the image
188     * @param hotspotX the X coordinate of the cursor's hot spot
189     * @param hotspotY the Y coordinate of the cursor's hot spot
190     */
191    public ImageCursor(final Image image,
192                       double hotspotX,
193                       double hotspotY) {
194        if ((image != null) && (image.getProgress() < 1)) {
195            DelayedInitialization.applyTo(
196                    this, image, hotspotX, hotspotY);
197        } else {
198            initialize(image, hotspotX, hotspotY);
199        }
200    }
201
202    /**
203     * Gets the supported cursor size that is closest to the specified preferred
204     * size. A value of (0,0) is returned if the platform does not support
205     * custom cursors.
206     *
207     * <p>
208     * Note: if an image is used whose dimensions don't match a supported size
209     * (as returned by this method), the implementation will resize the image to
210     * a supported size. This may result in a loss of quality.
211     *
212     * <p>
213     * Note: These values can vary between operating systems, graphics cards and
214     * screen resolution, but at the time of this writing, a sample Windows
215     * Vista machine returned 32x32 for all requested sizes, while sample Mac
216     * and Linux machines returned the requested size up to a maximum of 64x64.
217     * Applications should provide a 32x32 cursor, which will work well on all
218     * platforms, and may optionally wish to provide a 64x64 cursor for those
219     * platforms on which it is supported.
220     *
221     * @param preferredWidth the preferred width of the cursor
222     * @param preferredHeight the preferred height of the cursor
223     * @return the supported cursor size
224     * @since JavaFX 1.3
225     */
226    public static Dimension2D getBestSize(double preferredWidth,
227                                          double preferredHeight) {
228        return Toolkit.getToolkit().getBestCursorSize((int) preferredWidth,
229                                                      (int) preferredHeight);
230    }
231
232    /**
233     * Returns the maximum number of colors supported in a custom image cursor
234     * palette.
235     *
236     * <p>
237     * Note: if an image is used which has more colors in its palette than the
238     * supported maximum, the implementation will attempt to flatten the
239     * palette to the maximum. This may result in a loss of quality.
240     *
241     * <p>
242     * Note: These values can vary between operating systems, graphics cards and
243     * screen resolution,  but at the time of this writing, a sample Windows
244     * Vista machine returned 256, a sample Mac machine returned
245     * Integer.MAX_VALUE, indicating support for full color cursors, and
246     * a sample Linux machine returned 2. Applications may want to target these
247     * three color depths for an optimal cursor on each platform.
248     *
249     * @return the maximum number of colors supported in a custom image cursor
250     *      palette
251     * @since JavaFX 1.3
252     */
253    public static int getMaximumColors() {
254        return Toolkit.getToolkit().getMaximumCursorColors();
255    }
256
257    /**
258     * Creates a custom image cursor from one of the specified images. This function
259     * will choose the image whose size most closely matched the best cursor size.
260     * The hotpotX of the returned ImageCursor is scaled by
261     * chosenImage.width/images[0].width and the hotspotY is scaled by
262     * chosenImage.height/images[0].height.
263     * <p>
264     * On platforms that don't support custom cursors, {@code Cursor.DEFAULT} will
265     * be used in place of the returned ImageCursor.
266     *
267     * @param images a sequence of images from which to choose, in order of preference
268     * @param hotspotX the X coordinate of the hotspot within the first image
269     *        in the images sequence
270     * @param hotspotY the Y coordinate of the hotspot within the first image
271     *        in the images sequence
272     * @return a cursor created from the best image
273     * @since JavaFX 1.3
274     */
275    public static ImageCursor chooseBestCursor(
276            final Image[] images, final double hotspotX, final double hotspotY) {
277        final ImageCursor imageCursor = new ImageCursor();
278
279        if (needsDelayedInitialization(images)) {
280            DelayedInitialization.applyTo(
281                    imageCursor, images, hotspotX, hotspotY);
282        } else {
283            imageCursor.initialize(images, hotspotX, hotspotY);
284        }
285
286        return imageCursor;
287    }
288
289    @Override CursorFrame getCurrentFrame() {
290        if (currentCursorFrame != null) {
291            return currentCursorFrame;
292        }
293
294        final Image cursorImage = getImage();
295
296        if (cursorImage == null) {
297            currentCursorFrame = Cursor.DEFAULT.getCurrentFrame();
298            return currentCursorFrame;
299        }
300
301        final Object cursorPlatformImage = cursorImage.impl_getPlatformImage();
302        if (cursorPlatformImage == null) {
303            currentCursorFrame = Cursor.DEFAULT.getCurrentFrame();
304            return currentCursorFrame;
305        }
306
307        if (firstCursorFrame == null) {
308            firstCursorFrame =
309                    new ImageCursorFrame(cursorPlatformImage,
310                                         cursorImage.getWidth(),
311                                         cursorImage.getHeight(),
312                                         getHotspotX(),
313                                         getHotspotY());
314            currentCursorFrame = firstCursorFrame;
315        } else if (firstCursorFrame.getPlatformImage() == cursorPlatformImage) {
316            currentCursorFrame = firstCursorFrame;
317        } else {
318            if (otherCursorFrames == null) {
319                otherCursorFrames = new HashMap<Object, ImageCursorFrame>();
320            }
321
322            currentCursorFrame = otherCursorFrames.get(cursorPlatformImage);
323            if (currentCursorFrame == null) {
324                // cursor frame not created yet
325                final ImageCursorFrame newCursorFrame =
326                        new ImageCursorFrame(cursorPlatformImage,
327                                             cursorImage.getWidth(),
328                                             cursorImage.getHeight(),
329                                             getHotspotX(),
330                                             getHotspotY());
331
332                otherCursorFrames.put(cursorPlatformImage, newCursorFrame);
333                currentCursorFrame = newCursorFrame;
334            }
335        }
336
337        return currentCursorFrame;
338     }
339
340    private void invalidateCurrentFrame() {
341        currentCursorFrame = null;
342    }
343
344    @Override
345    void activate() {
346        if (++activeCounter == 1) {
347            bindImage(getImage());
348            invalidateCurrentFrame();
349        }
350    }
351
352    @Override
353    void deactivate() {
354        if (--activeCounter == 0) {
355            unbindImage(getImage());
356        }
357    }
358
359    private void initialize(final Image[] images,
360                            final double hotspotX,
361                            final double hotspotY) {
362        final Dimension2D dim = getBestSize(1f, 1f);
363
364        // If no valid image or if custom cursors are not supported, leave
365        // the default image cursor
366        if ((images.length == 0) || (dim.getWidth() == 0f)
367                                 || (dim.getHeight() == 0f)) {
368            return;
369        }
370
371        // If only a single image, use it to construct a custom cursor
372        if (images.length == 1) {
373            initialize(images[0], hotspotX, hotspotY);
374            return;
375        }
376
377        final Image bestImage = findBestImage(images);
378        final double scaleX = bestImage.getWidth() / images[0].getWidth();
379        final double scaleY = bestImage.getHeight() / images[0].getHeight();
380
381        initialize(bestImage, hotspotX * scaleX, hotspotY * scaleY);
382    }
383
384    private void initialize(Image newImage,
385                            double newHotspotX,
386                            double newHotspotY) {
387        final Image oldImage = getImage();
388        final double oldHotspotX = getHotspotX();
389        final double oldHotspotY = getHotspotY();
390
391        if ((newImage == null) || (newImage.getWidth() < 1f)
392                               || (newImage.getHeight() < 1f)) {
393            // If image is invalid set the hotspot to 0
394            newHotspotX = 0f;
395            newHotspotY = 0f;
396        } else {
397            if (newHotspotX < 0f) {
398                newHotspotX = 0f;
399            }
400            if (newHotspotX > (newImage.getWidth() - 1f)) {
401                newHotspotX = newImage.getWidth() - 1f;
402            }
403            if (newHotspotY < 0f) {
404                newHotspotY = 0f;
405            }
406            if (newHotspotY > (newImage.getHeight() - 1f)) {
407                newHotspotY = newImage.getHeight() - 1f;
408            }
409        }
410
411        imagePropertyImpl().store(newImage);
412        hotspotXPropertyImpl().store(newHotspotX);
413        hotspotYPropertyImpl().store(newHotspotY);
414
415        if (oldImage != newImage) {
416            if (activeCounter > 0) {
417                unbindImage(oldImage);
418                bindImage(newImage);
419            }
420
421            invalidateCurrentFrame();
422            image.fireValueChangedEvent();
423        }
424
425        if (oldHotspotX != newHotspotX) {
426            hotspotX.fireValueChangedEvent();
427        }
428
429        if (oldHotspotY != newHotspotY) {
430            hotspotY.fireValueChangedEvent();
431        }
432    }
433
434    private InvalidationListener imageListener;
435
436    private InvalidationListener getImageListener() {
437        if (imageListener == null) {
438            imageListener = new InvalidationListener() {
439                                @Override
440                                public void invalidated(Observable valueModel) {
441                                     invalidateCurrentFrame();
442                                }
443                             };
444        }
445
446        return imageListener;
447    }
448
449    private void bindImage(final Image toImage) {
450        if (toImage == null) {
451            return;
452        }
453
454        Toolkit.getImageAccessor().getImageProperty(toImage).addListener(getImageListener());
455    }
456
457    private void unbindImage(final Image fromImage) {
458        if (fromImage == null) {
459            return;
460        }
461
462        Toolkit.getImageAccessor().getImageProperty(fromImage).removeListener(getImageListener());
463    }
464
465    private static boolean needsDelayedInitialization(final Image[] images) {
466        for (final Image image: images) {
467            if (image.getProgress() < 1) {
468                return true;
469            }
470        }
471
472        return false;
473    }
474
475    // Utility function to select the best image
476    private static Image findBestImage(final Image[] images) {
477        // Check for exact match and return the first such match
478        for (final Image image: images) {
479            final Dimension2D dim = getBestSize((int) image.getWidth(),
480                                                (int) image.getHeight());
481            if ((dim.getWidth() == image.getWidth())
482                    && (dim.getHeight() == image.getHeight())) {
483                return image;
484            }
485        }
486
487        // No exact match, check for closest match without down-scaling
488        // (i.e., smallest scale >= 1.0)
489        Image bestImage = null;
490        double bestRatio = Double.MAX_VALUE;
491        for (final Image image: images) {
492            if ((image.getWidth() > 0) && (image.getHeight() > 0)) {
493                final Dimension2D dim = getBestSize(image.getWidth(),
494                                                    image.getHeight());
495                final double ratioX = dim.getWidth() / image.getWidth();
496                final double ratioY = dim.getHeight() / image.getHeight();
497                if ((ratioX >= 1) && (ratioY >= 1)) {
498                    final double ratio = Math.max(ratioX, ratioY);
499                    if (ratio < bestRatio) {
500                        bestImage = image;
501                        bestRatio = ratio;
502                    }
503                }
504            }
505        }
506        if (bestImage != null) {
507            return bestImage;
508        }
509
510        // Still no match, check for closest match alowing for down-scaling
511        // (i.e., smallest up-scale or down-scale >= 1.0)
512        for (final Image image: images) {
513            if ((image.getWidth() > 0) && (image.getHeight() > 0)) {
514                final Dimension2D dim = getBestSize(image.getWidth(),
515                                                    image.getHeight());
516                if ((dim.getWidth() > 0) && (dim.getHeight() > 0)) {
517                    double ratioX = dim.getWidth() / image.getWidth();
518                    if (ratioX < 1) {
519                        ratioX = 1 / ratioX;
520                    }
521                    double ratioY = dim.getHeight() / image.getHeight();
522                    if (ratioY < 1) {
523                        ratioY = 1 / ratioY;
524                    }
525                    final double ratio = Math.max(ratioX, ratioY);
526                    if (ratio < bestRatio) {
527                        bestImage = image;
528                        bestRatio = ratio;
529                    }
530                }
531            }
532        }
533        if (bestImage != null) {
534            return bestImage;
535        }
536
537        return images[0];
538    }
539
540    private final class DoublePropertyImpl extends ReadOnlyDoublePropertyBase {
541        private final String name;
542
543        private double value;
544
545        public DoublePropertyImpl(final String name) {
546            this.name = name;
547        }
548
549        public void store(final double value) {
550            this.value = value;
551        }
552
553        @Override
554        public void fireValueChangedEvent() {
555            super.fireValueChangedEvent();
556        }
557
558        @Override
559        public double get() {
560            return value;
561        }
562
563        @Override
564        public Object getBean() {
565            return ImageCursor.this;
566        }
567
568        @Override
569        public String getName() {
570            return name;
571        }
572    }
573
574    private final class ObjectPropertyImpl<T>
575            extends ReadOnlyObjectPropertyBase<T> {
576        private final String name;
577
578        private T value;
579
580        public ObjectPropertyImpl(final String name) {
581            this.name = name;
582        }
583
584        public void store(final T value) {
585            this.value = value;
586        }
587
588        @Override
589        public void fireValueChangedEvent() {
590            super.fireValueChangedEvent();
591        }
592
593        @Override
594        public T get() {
595            return value;
596        }
597
598        @Override
599        public Object getBean() {
600            return ImageCursor.this;
601        }
602
603        @Override
604        public String getName() {
605            return name;
606        }
607    }
608
609    private static final class DelayedInitialization
610            implements InvalidationListener {
611        private final ImageCursor targetCursor;
612
613        private final Image[] images;
614        private final double hotspotX;
615        private final double hotspotY;
616
617        private final boolean initAsSingle;
618
619        private int waitForImages;
620
621        private DelayedInitialization(final ImageCursor targetCursor,
622                                      final Image[] images,
623                                      final double hotspotX,
624                                      final double hotspotY,
625                                      final boolean initAsSingle) {
626            this.targetCursor = targetCursor;
627            this.images = images;
628            this.hotspotX = hotspotX;
629            this.hotspotY = hotspotY;
630            this.initAsSingle = initAsSingle;
631        }
632
633
634        public static void applyTo(final ImageCursor imageCursor,
635                                   final Image[] images,
636                                   final double hotspotX,
637                                   final double hotspotY) {
638            final DelayedInitialization delayedInitialization =
639                    new DelayedInitialization(imageCursor,
640                                              Arrays.copyOf(images, images.length),
641                                              hotspotX,
642                                              hotspotY,
643                                              false);
644            delayedInitialization.start();
645        }
646
647        public static void applyTo(final ImageCursor imageCursor,
648                                   final Image image,
649                                   final double hotspotX,
650                                   final double hotspotY) {
651            final DelayedInitialization delayedInitialization =
652                    new DelayedInitialization(imageCursor,
653                                              new Image[] { image },
654                                              hotspotX,
655                                              hotspotY,
656                                              true);
657            delayedInitialization.start();
658        }
659
660        private void start() {
661            for (final Image image: images) {
662                if (image.getProgress() < 1) {
663                    ++waitForImages;
664                    image.progressProperty().addListener(this);
665                }
666            }
667        }
668
669        private void cleanupAndFinishInitialization() {
670            for (final Image image: images) {
671                image.progressProperty().removeListener(this);
672            }
673
674            if (initAsSingle) {
675                targetCursor.initialize(images[0], hotspotX, hotspotY);
676            } else {
677                targetCursor.initialize(images, hotspotX, hotspotY);
678            }
679        }
680
681        @Override
682        public void invalidated(Observable valueModel) {
683            if (((ReadOnlyDoubleProperty)valueModel).get() == 1) {
684                if (--waitForImages == 0) {
685                    cleanupAndFinishInitialization();
686                }
687            }
688        }
689    }
690}