1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.scene.control.behavior;
  27 
  28 import com.sun.javafx.util.Utils;
  29 import javafx.animation.KeyFrame;
  30 import javafx.animation.Timeline;
  31 import javafx.event.ActionEvent;
  32 import javafx.event.EventHandler;
  33 import javafx.event.EventType;
  34 import javafx.geometry.NodeOrientation;
  35 import javafx.geometry.Orientation;
  36 import javafx.scene.control.Control;
  37 import javafx.scene.control.ScrollBar;
  38 import javafx.scene.control.Skin;
  39 import javafx.scene.input.KeyCode;
  40 import javafx.scene.input.KeyEvent;
  41 import javafx.util.Duration;
  42 
  43 import java.util.ArrayList;
  44 import java.util.List;
  45 
  46 import static javafx.scene.input.KeyCode.*;
  47 import static javafx.scene.input.KeyEvent.KEY_RELEASED;
  48 
  49 /**
  50  * A Behavior implementation for ScrollBars.
  51  *
  52  */
  53 
  54 public class ScrollBarBehavior extends BehaviorBase<ScrollBar> {
  55 
  56     /***************************************************************************
  57      *                                                                         *
  58      * Constructors                                                            *
  59      *                                                                         *
  60      **************************************************************************/
  61 
  62     public ScrollBarBehavior(ScrollBar scrollBar) {
  63         super(scrollBar, SCROLL_BAR_BINDINGS);
  64     }
  65 
  66     /***************************************************************************
  67      *                                                                         *
  68      * Functions                                                               *
  69      *                                                                         *
  70      **************************************************************************/
  71 
  72     void home() {
  73         getControl().setValue(getControl().getMin());
  74     }
  75 
  76     void decrementValue() {
  77         getControl().adjustValue(0);
  78     }
  79 
  80     void end() {
  81         getControl().setValue(getControl().getMax());
  82     }
  83 
  84     void incrementValue() {
  85         getControl().adjustValue(1);
  86     }
  87 
  88     /***************************************************************************
  89      *                                                                         *
  90      * Key event handling                                                      *
  91      *                                                                         *
  92      **************************************************************************/
  93 
  94     /* We manually specify the focus traversal keys because Slider has
  95      * different usage for up/down arrow keys.
  96      */
  97     protected static final List<KeyBinding> SCROLL_BAR_BINDINGS = new ArrayList<>();
  98     static {
  99         SCROLL_BAR_BINDINGS.add(new KeyBinding(F4, "TraverseDebug").alt().ctrl().shift());
 100 
 101         SCROLL_BAR_BINDINGS.add(new ScrollBarKeyBinding(LEFT, "DecrementValue"));
 102         SCROLL_BAR_BINDINGS.add(new ScrollBarKeyBinding(KP_LEFT, "DecrementValue"));
 103         SCROLL_BAR_BINDINGS.add(new ScrollBarKeyBinding(UP, "DecrementValue").vertical());
 104         SCROLL_BAR_BINDINGS.add(new ScrollBarKeyBinding(KP_UP, "DecrementValue").vertical());
 105         SCROLL_BAR_BINDINGS.add(new ScrollBarKeyBinding(RIGHT, "IncrementValue"));
 106         SCROLL_BAR_BINDINGS.add(new ScrollBarKeyBinding(KP_RIGHT, "IncrementValue"));
 107         SCROLL_BAR_BINDINGS.add(new ScrollBarKeyBinding(DOWN, "IncrementValue").vertical());
 108         SCROLL_BAR_BINDINGS.add(new ScrollBarKeyBinding(KP_DOWN, "IncrementValue").vertical());
 109 
 110         SCROLL_BAR_BINDINGS.add(new KeyBinding(HOME, KEY_RELEASED, "Home"));
 111         SCROLL_BAR_BINDINGS.add(new KeyBinding(END, KEY_RELEASED, "End"));
 112     }
 113 
 114     protected /*final*/ String matchActionForEvent(KeyEvent e) {
 115         String action = super.matchActionForEvent(e);
 116         if (action != null) {
 117             if (e.getCode() == LEFT || e.getCode() == KP_LEFT) {
 118                 if (getControl().getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) {
 119                     action = getControl().getOrientation() == Orientation.HORIZONTAL ? "IncrementValue" : "DecrementValue";
 120                 }
 121             } else if (e.getCode() == RIGHT || e.getCode() == KP_RIGHT) {
 122                 if (getControl().getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) {
 123                     action = getControl().getOrientation() == Orientation.HORIZONTAL ? "DecrementValue" : "IncrementValue";
 124                 }
 125             }
 126         }
 127         return action;
 128     }
 129 
 130     @Override protected void callAction(String name) {
 131         if ("Home".equals(name)) home();
 132         else if ("End".equals(name)) end();
 133         else if ("IncrementValue".equals(name)) incrementValue();
 134         else if ("DecrementValue".equals(name)) decrementValue();
 135         else super.callAction(name);
 136         super.callAction(name);
 137     }
 138 
 139     /***************************************************************************
 140      *                                                                         *
 141      * Mouse event handling                                                    *
 142      *                                                                         *
 143      **************************************************************************/
 144 
 145     /**
 146      * This timeline is used to adjust the value of the bar when the
 147      * track has been pressed but not released.
 148      */
 149     Timeline timeline;
 150 
 151     /**
 152      * Invoked by the ScrollBar {@link Skin} implementation whenever a mouse
 153      * press occurs on the "track" of the bar. This will cause the thumb to
 154      * be moved by some amount.
 155      *
 156      * @param position The mouse position on track with 0.0 being beginning of track and 1.0 being the end
 157      */
 158     public void trackPress(double position) {
 159 
 160         /* We can get a press if someone presses an end button.  In that
 161          * case, we don't want to start a timeline because the end button
 162          * will have already done so.  We can detect that because the timeline
 163          * will not be null.
 164          */
 165         if (timeline != null) return;
 166 
 167         // determine the percentage of the way between min and max
 168         // represented by this mouse event
 169         final ScrollBar bar = getControl();
 170         if (!bar.isFocused() && bar.isFocusTraversable()) bar.requestFocus();
 171         final double pos = position;
 172         final boolean incrementing = (pos > ((bar.getValue() - bar.getMin())/(bar.getMax() - bar.getMin())));
 173         timeline = new Timeline();
 174         timeline.setCycleCount(Timeline.INDEFINITE);
 175 
 176         final EventHandler<ActionEvent> step =
 177                 event -> {
 178                     boolean i = (pos > ((bar.getValue() - bar.getMin())/(bar.getMax() - bar.getMin())));
 179                     if (incrementing == i) {
 180                         // we started incrementing and still are, or we
 181                         // started decrementing and still are
 182                         bar.adjustValue(pos);
 183                     }
 184                     else {
 185                         stopTimeline();
 186                     }
 187                 };
 188 
 189         final KeyFrame kf = new KeyFrame(Duration.millis(200), step);
 190         timeline.getKeyFrames().add(kf);
 191         // do the first step immediately
 192         timeline.play();
 193         step.handle(null);
 194     }
 195 
 196     /**
 197      */
 198     public void trackRelease() {
 199         stopTimeline();
 200     }
 201 
 202     /**
 203      * Invoked by the ScrollBar {@link Skin} implementation whenever a mouse
 204      * press occurs on the decrement button of the bar.
 205      */
 206     public void decButtonPressed() {
 207         final ScrollBar bar = getControl();
 208         if (!bar.isFocused() && bar.isFocusTraversable()) bar.requestFocus();
 209         stopTimeline();
 210         timeline = new Timeline();
 211         timeline.setCycleCount(Timeline.INDEFINITE);
 212 
 213         final EventHandler<ActionEvent> dec =
 214                 event -> {
 215                     if (bar.getValue() > bar.getMin()) {
 216                         bar.decrement();
 217                     }
 218                     else {
 219                         stopTimeline();
 220                     }
 221                 };
 222 
 223         final KeyFrame kf = new KeyFrame(Duration.millis(200), dec);
 224         timeline.getKeyFrames().add(kf);
 225         // do the first step immediately
 226         timeline.play();
 227         dec.handle(null);
 228     }
 229 
 230     /**
 231      */
 232     public void decButtonReleased() {
 233         stopTimeline();
 234     }
 235 
 236     /**
 237      * Invoked by the ScrollBar {@link Skin} implementation whenever a mouse
 238      * press occurs on the increment button of the bar.
 239      */
 240     public void incButtonPressed() {
 241         final ScrollBar bar = getControl();
 242         if (!bar.isFocused() && bar.isFocusTraversable()) bar.requestFocus();
 243         stopTimeline();
 244         timeline = new Timeline();
 245         timeline.setCycleCount(Timeline.INDEFINITE);
 246 
 247         final EventHandler<ActionEvent> inc =
 248                 event -> {
 249                     if (bar.getValue() < bar.getMax()) {
 250                         bar.increment();
 251                     }
 252                     else {
 253                         stopTimeline();
 254                     }
 255                 };
 256 
 257         final KeyFrame kf = new KeyFrame(Duration.millis(200), inc);
 258         timeline.getKeyFrames().add(kf);
 259         // do the first step immediately
 260         timeline.play();
 261         inc.handle(null);
 262     }
 263 
 264     /**
 265      */
 266     public void incButtonReleased() {
 267         stopTimeline();
 268     }
 269 
 270     /**
 271      * @param position The mouse position on track with 0.0 being begining of track and 1.0 being the end
 272      */
 273     //public function thumbPressed(e:MouseEvent, position:Number):Void {
 274     //}
 275 
 276     /**
 277      * @param position The mouse position on track with 0.0 being begining of track and 1.0 being the end
 278      */
 279     public void thumbDragged(double position) {
 280         final ScrollBar scrollbar = getControl();
 281 
 282         // Stop the timeline for continuous increments as drags take precedence
 283         stopTimeline();
 284 
 285         if (!scrollbar.isFocused() && scrollbar.isFocusTraversable()) scrollbar.requestFocus();
 286         double newValue = (position * (scrollbar.getMax() - scrollbar.getMin())) + scrollbar.getMin();
 287         if (!Double.isNaN(newValue)) {
 288             scrollbar.setValue(Utils.clamp(scrollbar.getMin(), newValue, scrollbar.getMax()));
 289         }
 290     }
 291 
 292     private void stopTimeline() {
 293         if (timeline != null) {
 294             timeline.stop();
 295             timeline = null;
 296         }
 297     }
 298 
 299     /**
 300      * Class to handle key bindings based upon the orientation of the control.
 301      */
 302     public static class ScrollBarKeyBinding extends OrientedKeyBinding {
 303         public ScrollBarKeyBinding(KeyCode code, String action) {
 304             super(code, action);
 305         }
 306 
 307         public ScrollBarKeyBinding(KeyCode code, EventType<KeyEvent> type, String action) {
 308             super(code, type, action);
 309         }
 310 
 311         public @Override boolean getVertical(Control control) {
 312             return ((ScrollBar)control).getOrientation() == Orientation.VERTICAL;
 313         }
 314     }
 315 }