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