View Javadoc
1   /*
2    * Copyright 2000-2018 Vaadin Ltd.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5    * use this file except in compliance with the License. You may obtain a copy of
6    * the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations under
14   * the License.
15   */
16  package com.vaadin.client.ui;
17  
18  import com.google.gwt.core.client.Scheduler;
19  import com.google.gwt.dom.client.Element;
20  import com.google.gwt.dom.client.Style.Display;
21  import com.google.gwt.dom.client.Style.Overflow;
22  import com.google.gwt.dom.client.Style.Unit;
23  import com.google.gwt.event.dom.client.KeyCodes;
24  import com.google.gwt.event.logical.shared.ValueChangeEvent;
25  import com.google.gwt.event.logical.shared.ValueChangeHandler;
26  import com.google.gwt.event.shared.HandlerRegistration;
27  import com.google.gwt.user.client.Command;
28  import com.google.gwt.user.client.DOM;
29  import com.google.gwt.user.client.Event;
30  import com.google.gwt.user.client.Window;
31  import com.google.gwt.user.client.ui.HasValue;
32  import com.vaadin.client.ApplicationConnection;
33  import com.vaadin.client.BrowserInfo;
34  import com.vaadin.client.WidgetUtil;
35  import com.vaadin.shared.ui.slider.SliderOrientation;
36  
37  public class VSliderPatched extends SimpleFocusablePanel
38          implements Field, HasValue<Double>, SubPartAware {
39  
40      public static final String CLASSNAME = "v-slider";
41  
42      /**
43       * Minimum size (width or height, depending on orientation) of the slider
44       * base.
45       */
46      private static final int MIN_SIZE = 50;
47  
48      protected ApplicationConnection client;
49  
50      protected String id;
51  
52      protected boolean disabled;
53      protected boolean readonly;
54  
55      private int acceleration = 1;
56      protected double min;
57      protected double max;
58      protected int resolution;
59      protected Double value;
60  
61      private boolean updateValueOnClick;
62      protected SliderOrientation orientation = SliderOrientation.HORIZONTAL;
63  
64      /* DOM element for slider's base */
65      private final Element base;
66      private static final int BASE_BORDER_WIDTH = 1;
67  
68      /* DOM element for slider's handle */
69      private final Element handle;
70  
71      /* DOM element for decrement arrow */
72      private final Element smaller;
73  
74      /* DOM element for increment arrow */
75      private final Element bigger;
76  
77      /* Temporary dragging/animation variables */
78      private boolean dragging = false;
79  
80      private VLazyExecutor delayedValueUpdater = new VLazyExecutor(100, () -> {
81          fireValueChanged();
82          acceleration = 1;
83      });
84  
85      public VSliderPatched() {
86          super();
87  
88          base = DOM.createDiv();
89          handle = DOM.createDiv();
90          smaller = DOM.createDiv();
91          bigger = DOM.createDiv();
92  
93          setStyleName(CLASSNAME);
94  
95          getElement().appendChild(bigger);
96          getElement().appendChild(smaller);
97          getElement().appendChild(base);
98          base.appendChild(handle);
99  
100         // Hide initially
101         smaller.getStyle().setDisplay(Display.NONE);
102         bigger.getStyle().setDisplay(Display.NONE);
103 
104         sinkEvents(Event.MOUSEEVENTS | Event.ONMOUSEWHEEL | Event.KEYEVENTS
105                 | Event.FOCUSEVENTS | Event.TOUCHEVENTS);
106     }
107 
108     @Override
109     public void setStyleName(String style) {
110         updateStyleNames(style, false);
111     }
112 
113     @Override
114     public void setStylePrimaryName(String style) {
115         updateStyleNames(style, true);
116     }
117 
118     protected void updateStyleNames(String styleName,
119             boolean isPrimaryStyleName) {
120 
121         removeStyleName(getStylePrimaryName() + "-vertical");
122 
123         if (isPrimaryStyleName) {
124             super.setStylePrimaryName(styleName);
125         } else {
126             super.setStyleName(styleName);
127         }
128 
129         base.setClassName(getStylePrimaryName() + "-base");
130         handle.setClassName(getStylePrimaryName() + "-handle");
131         smaller.setClassName(getStylePrimaryName() + "-smaller");
132         bigger.setClassName(getStylePrimaryName() + "-bigger");
133 
134         if (isVertical()) {
135             addStyleName(getStylePrimaryName() + "-vertical");
136         }
137     }
138 
139     /** For internal use only. May be removed or replaced in the future. */
140     public void buildBase() {
141         final String styleAttribute = isVertical() ? "height" : "width";
142         final String oppositeStyleAttribute = isVertical() ? "width" : "height";
143         final String domProperty = isVertical() ? "offsetHeight"
144                 : "offsetWidth";
145 
146         // clear unnecessary opposite style attribute
147         base.getStyle().clearProperty(oppositeStyleAttribute);
148 
149         /*
150          * To resolve defect #13681 we should not return from method buildBase()
151          * if slider has no parentElement, because such operations as
152          * buildHandle() and setValues(), which are needed for Slider, are
153          * called at the end of method buildBase(). And these methods will not
154          * be called if there is no parentElement. So, instead of returning from
155          * method buildBase() if there is no parentElement "if condition" is
156          * applied to call code for parentElement only in case it exists.
157          */
158         if (getElement().hasParentElement()) {
159             final Element p = getElement();
160             if (p.getPropertyInt(domProperty) > MIN_SIZE) {
161                 if (isVertical()) {
162                     setHeight();
163                 } else {
164                     base.getStyle().clearProperty(styleAttribute);
165                 }
166             } else {
167                 // Set minimum size and adjust after all components have
168                 // (supposedly) been drawn completely.
169                 base.getStyle().setPropertyPx(styleAttribute, MIN_SIZE);
170                 Scheduler.get().scheduleDeferred(new Command() {
171 
172                     @Override
173                     public void execute() {
174                         final Element p = getElement();
175                         if (p.getPropertyInt(domProperty) > MIN_SIZE + 5
176                                 || propertyNotNullOrEmpty(styleAttribute, p)) {
177                             if (isVertical()) {
178                                 setHeight();
179                             } else {
180                                 base.getStyle().clearProperty(styleAttribute);
181                             }
182                             // Ensure correct position
183                             setValue(value, false);
184                         }
185                     }
186 
187                     // Style has non empty property
188                     private boolean propertyNotNullOrEmpty(
189                             final String styleAttribute, final Element p) {
190                         return p.getStyle().getProperty(styleAttribute) != null
191                                 && !p.getStyle().getProperty(styleAttribute)
192                                         .isEmpty();
193                     }
194                 });
195             }
196         }
197 
198         if (!isVertical()) {
199             // Draw handle with a delay to allow base to gain maximum width
200             Scheduler.get().scheduleDeferred(() -> {
201                 buildHandle();
202                 setValue(value, false);
203             });
204         } else {
205             buildHandle();
206             setValue(value, false);
207         }
208 
209         // TODO attach listeners for focusing and arrow keys
210     }
211 
212     void buildHandle() {
213         final String handleAttribute = isVertical() ? "marginTop"
214                 : "marginLeft";
215         final String oppositeHandleAttribute = isVertical() ? "marginLeft"
216                 : "marginTop";
217 
218         handle.getStyle().setProperty(handleAttribute, "0");
219 
220         // clear unnecessary opposite handle attribute
221         handle.getStyle().clearProperty(oppositeHandleAttribute);
222     }
223 
224     @Override
225     public void onBrowserEvent(Event event) {
226         if (disabled || readonly) {
227             return;
228         }
229         final Element targ = DOM.eventGetTarget(event);
230         final Element slider = getElement();
231 
232         if (DOM.eventGetType(event) == Event.ONMOUSEWHEEL) {
233             processMouseWheelEvent(event);
234         } else if (dragging || targ == handle || targ == base || targ == slider) {
235             processHandleEvent(event);
236         } else if (targ.equals(base)
237                 && DOM.eventGetType(event) == Event.ONMOUSEUP
238                 && updateValueOnClick) {
239             processBaseEvent(event);
240         } else if (targ == smaller) {
241             decreaseValue(true);
242         } else if (targ == bigger) {
243             increaseValue(true);
244         } else if (isNavigationEvent(event)) {
245 
246             if (handleNavigation(event.getKeyCode(), event.getCtrlKey(),
247                     event.getShiftKey())) {
248 
249                 delayedValueUpdater.trigger();
250 
251                 DOM.eventPreventDefault(event);
252                 DOM.eventCancelBubble(event, true);
253             }
254         } else if (targ.equals(getElement())
255                 && DOM.eventGetType(event) == Event.ONFOCUS) {
256         } else if (targ.equals(getElement())
257                 && DOM.eventGetType(event) == Event.ONBLUR) {
258         } else if (DOM.eventGetType(event) == Event.ONMOUSEDOWN) {
259         }
260         if (WidgetUtil.isTouchEvent(event)) {
261             event.preventDefault(); // avoid simulated events
262             event.stopPropagation();
263         }
264     }
265 
266     private boolean isNavigationEvent(Event event) {
267         if (BrowserInfo.get().isGecko()
268                 && BrowserInfo.get().getGeckoVersion() < 65) {
269             return DOM.eventGetType(event) == Event.ONKEYPRESS;
270         } else {
271             return DOM.eventGetType(event) == Event.ONKEYDOWN;
272         }
273     }
274 
275     private void processMouseWheelEvent(final Event event) {
276         final int dir = DOM.eventGetMouseWheelVelocityY(event);
277 
278         if (dir < 0) {
279             increaseValue(false);
280         } else {
281             decreaseValue(false);
282         }
283 
284         delayedValueUpdater.trigger();
285 
286         DOM.eventPreventDefault(event);
287         DOM.eventCancelBubble(event, true);
288     }
289 
290     private void processHandleEvent(Event event) {
291         switch (DOM.eventGetType(event)) {
292         case Event.ONMOUSEDOWN:
293         case Event.ONTOUCHSTART:
294             if (!disabled && !readonly) {
295                 focus();
296                 dragging = true;
297                 handle.setClassName(getStylePrimaryName() + "-handle");
298                 handle.addClassName(getStylePrimaryName() + "-handle-active");
299 
300                 DOM.setCapture(getElement());
301                 DOM.eventPreventDefault(event); // prevent selecting text
302                 DOM.eventCancelBubble(event, true);
303                 setValueByEvent(event, true);
304                 event.stopPropagation();
305             }
306             break;
307         case Event.ONMOUSEMOVE:
308         case Event.ONTOUCHMOVE:
309             if (dragging) {
310                 setValueByEvent(event, true);
311                 event.stopPropagation();
312             }
313             break;
314         case Event.ONTOUCHEND:
315         case Event.ONMOUSEUP:
316             dragging = false;
317             handle.setClassName(getStylePrimaryName() + "-handle");
318             DOM.releaseCapture(getElement());
319             setValueByEvent(event, true);
320             event.stopPropagation();
321             break;
322         default:
323             break;
324         }
325     }
326 
327     private void processBaseEvent(Event event) {
328         if (!disabled && !readonly && !dragging) {
329             setValueByEvent(event, true);
330             DOM.eventCancelBubble(event, true);
331         }
332     }
333 
334     private void decreaseValue(boolean updateToServer) {
335         setValue(new Double(value.doubleValue() - Math.pow(10, -resolution)),
336                 updateToServer);
337     }
338 
339     private void increaseValue(boolean updateToServer) {
340         setValue(new Double(value.doubleValue() + Math.pow(10, -resolution)),
341                 updateToServer);
342     }
343 
344     private void setValueByEvent(Event event, boolean updateToServer) {
345         double v = min; // Fallback to min
346 
347         final int coord = getEventPosition(event);
348 
349         final int handleSize, baseSize, baseOffset;
350         if (isVertical()) {
351             handleSize = handle.getOffsetHeight();
352             baseSize = base.getOffsetHeight();
353             baseOffset = base.getAbsoluteTop() - Window.getScrollTop()
354                     - handleSize / 2;
355         } else {
356             handleSize = handle.getOffsetWidth();
357             baseSize = base.getOffsetWidth();
358             baseOffset = base.getAbsoluteLeft() - Window.getScrollLeft()
359                     + handleSize / 2;
360         }
361 
362         if (isVertical()) {
363             v = (baseSize - (coord - baseOffset))
364                     / (double) (baseSize - handleSize) * (max - min) + min;
365         } else {
366             v = (coord - baseOffset) / (double) (baseSize - handleSize)
367                     * (max - min) + min;
368         }
369 
370         if (v < min) {
371             v = min;
372         } else if (v > max) {
373             v = max;
374         }
375 
376         setValue(v, updateToServer);
377     }
378 
379     /**
380      * TODO consider extracting touches support to an impl class specific for
381      * webkit (only browser that really supports touches).
382      *
383      * @param event
384      * @return
385      */
386     protected int getEventPosition(Event event) {
387         if (isVertical()) {
388             return WidgetUtil.getTouchOrMouseClientY(event);
389         } else {
390             return WidgetUtil.getTouchOrMouseClientX(event);
391         }
392     }
393 
394     public void iLayout() {
395         if (isVertical()) {
396             setHeight();
397         }
398         // Update handle position
399         setValue(value, false);
400     }
401 
402     private void setHeight() {
403         // Calculate decoration size
404         base.getStyle().setHeight(0, Unit.PX);
405         base.getStyle().setOverflow(Overflow.HIDDEN);
406         int h = getElement().getOffsetHeight();
407         if (h < MIN_SIZE) {
408             h = MIN_SIZE;
409         }
410         base.getStyle().setHeight(h, Unit.PX);
411         base.getStyle().clearOverflow();
412     }
413 
414     private void fireValueChanged() {
415         ValueChangeEvent.fire(VSliderPatched.this, value);
416     }
417 
418     /**
419      * Handles the keyboard events handled by the Slider.
420      *
421      * @param keycode
422      *            The key code received
423      * @param ctrl
424      *            Whether {@code CTRL} was pressed
425      * @param shift
426      *            Whether {@code SHIFT} was pressed
427      * @return true if the navigation event was handled
428      */
429     public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
430 
431         // No support for ctrl moving
432         if (ctrl) {
433             return false;
434         }
435 
436         if (keycode == getNavigationUpKey() && isVertical()
437                 || keycode == getNavigationRightKey() && !isVertical()) {
438             if (shift) {
439                 for (int a = 0; a < acceleration; a++) {
440                     increaseValue(false);
441                 }
442                 acceleration++;
443             } else {
444                 increaseValue(false);
445             }
446             return true;
447         } else if (keycode == getNavigationDownKey() && isVertical()
448                 || keycode == getNavigationLeftKey() && !isVertical()) {
449             if (shift) {
450                 for (int a = 0; a < acceleration; a++) {
451                     decreaseValue(false);
452                 }
453                 acceleration++;
454             } else {
455                 decreaseValue(false);
456             }
457             return true;
458         }
459 
460         return false;
461     }
462 
463     /**
464      * Get the key that increases the vertical slider. By default it is the up
465      * arrow key but by overriding this you can change the key to whatever you
466      * want.
467      *
468      * @return The keycode of the key
469      */
470     protected int getNavigationUpKey() {
471         return KeyCodes.KEY_UP;
472     }
473 
474     /**
475      * Get the key that decreases the vertical slider. By default it is the down
476      * arrow key but by overriding this you can change the key to whatever you
477      * want.
478      *
479      * @return The keycode of the key
480      */
481     protected int getNavigationDownKey() {
482         return KeyCodes.KEY_DOWN;
483     }
484 
485     /**
486      * Get the key that decreases the horizontal slider. By default it is the
487      * left arrow key but by overriding this you can change the key to whatever
488      * you want.
489      *
490      * @return The keycode of the key
491      */
492     protected int getNavigationLeftKey() {
493         return KeyCodes.KEY_LEFT;
494     }
495 
496     /**
497      * Get the key that increases the horizontal slider. By default it is the
498      * right arrow key but by overriding this you can change the key to whatever
499      * you want.
500      *
501      * @return The keycode of the key
502      */
503     protected int getNavigationRightKey() {
504         return KeyCodes.KEY_RIGHT;
505     }
506 
507     public void setConnection(ApplicationConnection client) {
508         this.client = client;
509     }
510 
511     public void setId(String id) {
512         this.id = id;
513     }
514 
515     public void setDisabled(boolean disabled) {
516         this.disabled = disabled;
517     }
518 
519     public void setReadOnly(boolean readonly) {
520         this.readonly = readonly;
521     }
522 
523     private boolean isVertical() {
524         return orientation == SliderOrientation.VERTICAL;
525     }
526 
527     public void setOrientation(SliderOrientation orientation) {
528         if (this.orientation != orientation) {
529             this.orientation = orientation;
530             updateStyleNames(getStylePrimaryName(), true);
531         }
532     }
533 
534     public void setMinValue(double value) {
535         min = value;
536     }
537 
538     public void setMaxValue(double value) {
539         max = value;
540     }
541 
542     public void setResolution(int resolution) {
543         this.resolution = resolution;
544     }
545 
546     @Override
547     public HandlerRegistration addValueChangeHandler(
548             ValueChangeHandler<Double> handler) {
549         return addHandler(handler, ValueChangeEvent.getType());
550     }
551 
552     @Override
553     public Double getValue() {
554         return value;
555     }
556 
557     @Override
558     public void setValue(Double value) {
559         if (value < min) {
560             value = min;
561         } else if (value > max) {
562             value = max;
563         }
564 
565         // Update handle position
566         final String styleAttribute = isVertical() ? "marginTop" : "marginLeft";
567         final String domProperty = isVertical() ? "offsetHeight"
568                 : "offsetWidth";
569         final int handleSize = handle.getPropertyInt(domProperty);
570         final int baseSize = base.getPropertyInt(domProperty)
571                 - 2 * BASE_BORDER_WIDTH;
572 
573         final int range = baseSize - handleSize;
574         double v = value.doubleValue();
575 
576         // Round value to resolution
577         if (resolution > 0) {
578             v = Math.round(v * Math.pow(10, resolution));
579             v = v / Math.pow(10, resolution);
580         } else {
581             v = Math.round(v);
582         }
583         final double valueRange = max - min;
584         double p = 0;
585         if (valueRange > 0) {
586             p = range * ((v - min) / valueRange);
587         }
588         if (p < 0) {
589             p = 0;
590         }
591         if (isVertical()) {
592             p = range - p;
593         }
594         final double pos = p;
595 
596         handle.getStyle().setPropertyPx(styleAttribute, (int) Math.round(pos));
597 
598         // Update value
599         this.value = new Double(v);
600     }
601 
602     @Override
603     public void setValue(Double value, boolean fireEvents) {
604         if (value == null) {
605             return;
606         }
607 
608         setValue(value);
609 
610         if (fireEvents) {
611             fireValueChanged();
612         }
613     }
614 
615     @Override
616     public com.google.gwt.user.client.Element getSubPartElement(
617             String subPart) {
618         return null;
619     }
620 
621     @Override
622     public String getSubPartName(
623             com.google.gwt.user.client.Element subElement) {
624         return null;
625     }
626 
627     /**
628      * Specifies whether or not click event should update the Slider's value.
629      *
630      * @param updateValueOnClick
631      */
632     public void setUpdateValueOnClick(boolean updateValueOnClick) {
633         this.updateValueOnClick = updateValueOnClick;
634     }
635 }