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