1 /*
   2  * Copyright (c) 2010, 2015, 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 javafx.event.EventType;
  29 import javafx.geometry.NodeOrientation;
  30 import javafx.geometry.Orientation;
  31 import javafx.scene.control.Control;
  32 import javafx.scene.control.Skin;
  33 import javafx.scene.control.Slider;
  34 import javafx.scene.input.KeyCode;
  35 import javafx.scene.input.KeyEvent;
  36 import javafx.scene.input.MouseEvent;
  37 import java.util.ArrayList;
  38 import java.util.List;
  39 import com.sun.javafx.util.Utils;
  40 import static javafx.scene.input.KeyCode.DOWN;
  41 import static javafx.scene.input.KeyCode.END;
  42 import static javafx.scene.input.KeyCode.F4;
  43 import static javafx.scene.input.KeyCode.HOME;
  44 import static javafx.scene.input.KeyCode.KP_DOWN;
  45 import static javafx.scene.input.KeyCode.KP_LEFT;
  46 import static javafx.scene.input.KeyCode.KP_RIGHT;
  47 import static javafx.scene.input.KeyCode.KP_UP;
  48 import static javafx.scene.input.KeyCode.LEFT;
  49 import static javafx.scene.input.KeyCode.RIGHT;
  50 import static javafx.scene.input.KeyCode.UP;
  51 import static javafx.scene.input.KeyEvent.KEY_RELEASED;
  52 
  53 public class SliderBehavior extends BehaviorBase<Slider> {
  54     /**************************************************************************
  55      *                          Setup KeyBindings                             *
  56      *                                                                        *
  57      * We manually specify the focus traversal keys because Slider has        *
  58      * different usage for up/down arrow keys.                                *
  59      *************************************************************************/
  60     protected static final List<KeyBinding> SLIDER_BINDINGS = new ArrayList<KeyBinding>();
  61     static {
  62         SLIDER_BINDINGS.add(new KeyBinding(F4, "TraverseDebug").alt().ctrl().shift());
  63 
  64         SLIDER_BINDINGS.add(new SliderKeyBinding(LEFT, "DecrementValue"));
  65         SLIDER_BINDINGS.add(new SliderKeyBinding(KP_LEFT, "DecrementValue"));
  66         SLIDER_BINDINGS.add(new SliderKeyBinding(UP, "IncrementValue").vertical());
  67         SLIDER_BINDINGS.add(new SliderKeyBinding(KP_UP, "IncrementValue").vertical());
  68         SLIDER_BINDINGS.add(new SliderKeyBinding(RIGHT, "IncrementValue"));
  69         SLIDER_BINDINGS.add(new SliderKeyBinding(KP_RIGHT, "IncrementValue"));
  70         SLIDER_BINDINGS.add(new SliderKeyBinding(DOWN, "DecrementValue").vertical());
  71         SLIDER_BINDINGS.add(new SliderKeyBinding(KP_DOWN, "DecrementValue").vertical());
  72 
  73         SLIDER_BINDINGS.add(new KeyBinding(HOME, KEY_RELEASED, "Home"));
  74         SLIDER_BINDINGS.add(new KeyBinding(END, KEY_RELEASED, "End"));
  75     }
  76 
  77     protected /*final*/ String matchActionForEvent(KeyEvent e) {
  78         String action = super.matchActionForEvent(e);
  79         if (action != null) {
  80             if (e.getCode() == LEFT || e.getCode() == KP_LEFT) {
  81                 if (getControl().getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) {
  82                     action = getControl().getOrientation() == Orientation.HORIZONTAL ? "IncrementValue" : "DecrementValue";
  83                 }
  84             } else if (e.getCode() == RIGHT || e.getCode() == KP_RIGHT) {
  85                 if (getControl().getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) {
  86                     action = getControl().getOrientation() == Orientation.HORIZONTAL ? "DecrementValue" : "IncrementValue";
  87                 }
  88             }
  89         }
  90         return action;
  91     }
  92     
  93     @Override
  94     protected void callAction(String name) {
  95         if ("Home".equals(name)) home();
  96         else if ("End".equals(name)) end();
  97         else if ("IncrementValue".equals(name)) incrementValue();
  98         else if ("DecrementValue".equals(name)) decrementValue();
  99         else super.callAction(name);
 100     }
 101 
 102     private TwoLevelFocusBehavior tlFocus;
 103 
 104     public SliderBehavior(Slider slider) {
 105         super(slider, SLIDER_BINDINGS);
 106         // Only add this if we're on an embedded platform that supports 5-button navigation
 107         if (com.sun.javafx.scene.control.skin.Utils.isTwoLevelFocus()) {
 108             tlFocus = new TwoLevelFocusBehavior(slider); // needs to be last.
 109         }
 110     }
 111 
 112     @Override public void dispose() {
 113         if (tlFocus != null) tlFocus.dispose();
 114         super.dispose();
 115     }
 116 
 117     /**************************************************************************
 118      *                         State and Functions                            *
 119      *************************************************************************/
 120 
 121     /**
 122      * Invoked by the Slider {@link Skin} implementation whenever a mouse press
 123      * occurs on the "track" of the slider. This will cause the thumb to be
 124      * moved by some amount.
 125      *
 126      * @param position The mouse position on track with 0.0 being beginning of
 127      *        track and 1.0 being the end
 128      */
 129     public void trackPress(MouseEvent e, double position) {
 130         // determine the percentage of the way between min and max
 131         // represented by this mouse event
 132         final Slider slider = getControl();
 133         // If not already focused, request focus
 134         if (!slider.isFocused()) slider.requestFocus();
 135         if (slider.getOrientation().equals(Orientation.HORIZONTAL)) {
 136             slider.adjustValue(position * (slider.getMax() - slider.getMin()) + slider.getMin());
 137         } else {
 138             slider.adjustValue((1-position) * (slider.getMax() - slider.getMin()) + slider.getMin());
 139         }
 140     }
 141 
 142      /**
 143      * @param position The mouse position on track with 0.0 being beginning of
 144       *       track and 1.0 being the end
 145      */
 146     public void thumbPressed(MouseEvent e, double position) {
 147         // If not already focused, request focus
 148         final Slider slider = getControl();
 149         if (!slider.isFocused())  slider.requestFocus();
 150         slider.setValueChanging(true);
 151     }
 152 
 153     /**
 154      * @param position The mouse position on track with 0.0 being beginning of
 155      *        track and 1.0 being the end
 156      */
 157     public void thumbDragged(MouseEvent e, double position) {
 158         final Slider slider = getControl();
 159         slider.setValue(Utils.clamp(slider.getMin(), (position * (slider.getMax() - slider.getMin())) + slider.getMin(), slider.getMax()));
 160     }
 161 
 162     private double snapValueToTicks(double val) {
 163         final Slider slider = getControl();
 164         double v = val;
 165         double tickSpacing = 0;
 166         // compute the nearest tick to this value
 167         if (slider.getMinorTickCount() != 0) {
 168             tickSpacing = slider.getMajorTickUnit() / (Math.max(slider.getMinorTickCount(),0)+1);
 169         } else {
 170             tickSpacing = slider.getMajorTickUnit();
 171         }
 172         int prevTick = (int)((v - slider.getMin())/ tickSpacing);
 173         double prevTickValue = (prevTick) * tickSpacing + slider.getMin();
 174         double nextTickValue = (prevTick + 1) * tickSpacing + slider.getMin();
 175         v = Utils.nearest(prevTickValue, v, nextTickValue);
 176         return Utils.clamp(slider.getMin(), v, slider.getMax());
 177     }
 178 
 179 
 180     /**
 181      * When thumb is released valueChanging should be set to false.
 182      */
 183     public void thumbReleased(MouseEvent e) {
 184         final Slider slider = getControl();
 185         slider.setValueChanging(false);
 186         // RT-15207 When snapToTicks is true, slider value calculated in drag
 187         // is then snapped to the nearest tick on mouse release.
 188         if (slider.isSnapToTicks()) {
 189             slider.setValue(snapValueToTicks(slider.getValue()));
 190         }
 191     }
 192 
 193     void home() {
 194         final Slider slider = getControl();
 195         slider.adjustValue(slider.getMin());
 196     }
 197 
 198     void decrementValue() {
 199         final Slider slider = getControl();
 200         // RT-8634 If snapToTicks is true and block increment is less than
 201         // tick spacing, tick spacing is used as the decrement value.
 202         if (slider.isSnapToTicks()) {
 203             slider.adjustValue(slider.getValue() - computeIncrement());
 204         } else {
 205             slider.decrement();
 206         }
 207         
 208     }
 209 
 210     void end() {
 211         final Slider slider = getControl();
 212         slider.adjustValue(slider.getMax());
 213     }
 214 
 215     void incrementValue() {
 216         final Slider slider = getControl();
 217         // RT-8634 If snapToTicks is true and block increment is less than
 218         // tick spacing, tick spacing is used as the increment value.
 219         if (slider.isSnapToTicks()) {
 220             slider.adjustValue(slider.getValue()+ computeIncrement());
 221         } else {
 222             slider.increment();
 223         }
 224     }
 225 
 226     // Used only if snapToTicks is true.
 227     double computeIncrement() {
 228         final Slider slider = getControl();
 229         double tickSpacing = 0;
 230         if (slider.getMinorTickCount() != 0) {
 231             tickSpacing = slider.getMajorTickUnit() / (Math.max(slider.getMinorTickCount(),0)+1);
 232         } else {
 233             tickSpacing = slider.getMajorTickUnit();
 234         }
 235 
 236         if (slider.getBlockIncrement() > 0 && slider.getBlockIncrement() < tickSpacing) {
 237                 return tickSpacing;
 238         }
 239 
 240         return slider.getBlockIncrement();
 241     }
 242 
 243     public static class SliderKeyBinding extends OrientedKeyBinding {
 244         public SliderKeyBinding(KeyCode code, String action) {
 245             super(code, action);
 246         }
 247 
 248         public SliderKeyBinding(KeyCode code, EventType<KeyEvent> type, String action) {
 249             super(code, type, action);
 250         }
 251 
 252         public @Override boolean getVertical(Control control) {
 253             return ((Slider)control).getOrientation() == Orientation.VERTICAL;
 254         }
 255     }
 256 
 257 }