View Javadoc
1   /*
2    * Copyright 2000-2014 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  //
17  package com.vaadin.client.ui;
18  
19  import com.google.gwt.core.client.Scheduler;
20  import com.google.gwt.core.client.Scheduler.ScheduledCommand;
21  import com.google.gwt.dom.client.Element;
22  import com.google.gwt.dom.client.Style.Display;
23  import com.google.gwt.dom.client.Style.Overflow;
24  import com.google.gwt.dom.client.Style.Unit;
25  import com.google.gwt.event.dom.client.KeyCodes;
26  import com.google.gwt.event.logical.shared.ValueChangeEvent;
27  import com.google.gwt.event.logical.shared.ValueChangeHandler;
28  import com.google.gwt.event.shared.HandlerRegistration;
29  import com.google.gwt.user.client.Command;
30  import com.google.gwt.user.client.DOM;
31  import com.google.gwt.user.client.Event;
32  import com.google.gwt.user.client.Window;
33  import com.google.gwt.user.client.ui.HasValue;
34  import com.vaadin.client.ApplicationConnection;
35  import com.vaadin.client.BrowserInfo;
36  import com.vaadin.client.WidgetUtil;
37  import com.vaadin.shared.ui.slider.SliderOrientation;
38  
39  public class VSliderPatched extends SimpleFocusablePanel implements Field,
40          HasValue<Double>, SubPartAware {
41  
42      public static final String CLASSNAME = "v-slider";
43  
44      /**
45       * Minimum size (width or height, depending on orientation) of the slider
46       * base.
47       */
48      private static final int MIN_SIZE = 50;
49  
50      protected ApplicationConnection client;
51  
52      protected String id;
53  
54      protected boolean immediate;
55      protected boolean disabled;
56      protected boolean readonly;
57  
58      private int acceleration = 1;
59      protected double min;
60      protected double max;
61      protected int resolution;
62      protected Double value;
63      protected SliderOrientation orientation = SliderOrientation.HORIZONTAL;
64  
65      /* DOM element for slider's base */
66      private final Element base;
67      private final int BASE_BORDER_WIDTH = 1;
68  
69      /* DOM element for slider's handle */
70      private final Element handle;
71  
72      /* DOM element for decrement arrow */
73      private final Element smaller;
74  
75      /* DOM element for increment arrow */
76      private final Element bigger;
77  
78      /* Temporary dragging/animation variables */
79      private boolean dragging = false;
80  
81      private VLazyExecutor delayedValueUpdater = new VLazyExecutor(100,
82              new ScheduledCommand() {
83  
84                  @Override
85                  public void execute() {
86                      fireValueChanged();
87                      acceleration = 1;
88                  }
89              });
90  
91      public VSliderPatched() {
92          super();
93  
94          base = DOM.createDiv();
95          handle = DOM.createDiv();
96          smaller = DOM.createDiv();
97          bigger = DOM.createDiv();
98  
99          setStyleName(CLASSNAME);
100 
101         getElement().appendChild(bigger);
102         getElement().appendChild(smaller);
103         getElement().appendChild(base);
104         base.appendChild(handle);
105 
106         // Hide initially
107         smaller.getStyle().setDisplay(Display.NONE);
108         bigger.getStyle().setDisplay(Display.NONE);
109 
110         sinkEvents(Event.MOUSEEVENTS | Event.ONMOUSEWHEEL | Event.KEYEVENTS
111                 | Event.FOCUSEVENTS | Event.TOUCHEVENTS);
112 
113     }
114 
115     @Override
116     public void setStyleName(String style) {
117         updateStyleNames(style, false);
118     }
119 
120     @Override
121     public void setStylePrimaryName(String style) {
122         updateStyleNames(style, true);
123     }
124 
125     protected void updateStyleNames(String styleName, boolean isPrimaryStyleName) {
126 
127         removeStyleName(getStylePrimaryName() + "-vertical");
128 
129         if (isPrimaryStyleName) {
130             super.setStylePrimaryName(styleName);
131         } else {
132             super.setStyleName(styleName);
133         }
134 
135         base.setClassName(getStylePrimaryName() + "-base");
136         handle.setClassName(getStylePrimaryName() + "-handle");
137         smaller.setClassName(getStylePrimaryName() + "-smaller");
138         bigger.setClassName(getStylePrimaryName() + "-bigger");
139 
140         if (isVertical()) {
141             addStyleName(getStylePrimaryName() + "-vertical");
142         }
143     }
144 
145     /** For internal use only. May be removed or replaced in the future. */
146     public void buildBase() {
147         final String styleAttribute = isVertical() ? "height" : "width";
148         final String oppositeStyleAttribute = isVertical() ? "width" : "height";
149         final String domProperty = isVertical() ? "offsetHeight"
150                 : "offsetWidth";
151 
152         // clear unnecessary opposite style attribute
153         base.getStyle().clearProperty(oppositeStyleAttribute);
154 
155         /*
156          * To resolve defect #13681 we should not return from method buildBase()
157          * if slider has no parentElement, because such operations as
158          * buildHandle() and setValues(), which are needed for Slider, are
159          * called at the end of method buildBase(). And these methods will not
160          * be called if there is no parentElement. So, instead of returning from
161          * method buildBase() if there is no parentElement "if condition" is
162          * applied to call code for parentElement only in case it exists.
163          */
164         if (getElement().hasParentElement()) {
165             final Element p = getElement();
166             if (p.getPropertyInt(domProperty) > MIN_SIZE) {
167                 if (isVertical()) {
168                     setHeight();
169                 } else {
170                     base.getStyle().clearProperty(styleAttribute);
171                 }
172             } else {
173                 // Set minimum size and adjust after all components have
174                 // (supposedly) been drawn completely.
175                 base.getStyle().setPropertyPx(styleAttribute, MIN_SIZE);
176                 Scheduler.get().scheduleDeferred(new Command() {
177 
178                     @Override
179                     public void execute() {
180                         final Element p = getElement();
181                         if (p.getPropertyInt(domProperty) > (MIN_SIZE + 5)
182                                 || propertyNotNullOrEmpty(styleAttribute, p)) {
183                             if (isVertical()) {
184                                 setHeight();
185                             } else {
186                                 base.getStyle().clearProperty(styleAttribute);
187                             }
188                             // Ensure correct position
189                             setValue(value, false);
190                         }
191                     }
192 
193                     // Style has non empty property
194                     private boolean propertyNotNullOrEmpty(
195                             final String styleAttribute, final Element p) {
196                         return p.getStyle().getProperty(styleAttribute) != null
197                                 && !p.getStyle().getProperty(styleAttribute)
198                                         .isEmpty();
199                     }
200                 });
201             }
202         }
203 
204         if (!isVertical()) {
205             // Draw handle with a delay to allow base to gain maximum width
206             Scheduler.get().scheduleDeferred(new Command() {
207                 @Override
208                 public void execute() {
209                     buildHandle();
210                     setValue(value, false);
211                 }
212             });
213         } else {
214             buildHandle();
215             setValue(value, false);
216         }
217 
218         // TODO attach listeners for focusing and arrow keys
219     }
220 
221     void buildHandle() {
222         final String handleAttribute = isVertical() ? "marginTop"
223                 : "marginLeft";
224         final String oppositeHandleAttribute = isVertical() ? "marginLeft"
225                 : "marginTop";
226 
227         handle.getStyle().setProperty(handleAttribute, "0");
228 
229         // clear unnecessary opposite handle attribute
230         handle.getStyle().clearProperty(oppositeHandleAttribute);
231     }
232 
233     @Override
234     public void onBrowserEvent(Event event) {
235         if (disabled || readonly) {
236             return;
237         }
238         final Element targ = DOM.eventGetTarget(event);
239         final Element slider = getElement();
240 
241         if (DOM.eventGetType(event) == Event.ONMOUSEWHEEL) {
242             processMouseWheelEvent(event);
243         } else if (dragging || targ == handle || targ == base || targ == slider) {
244             processHandleEvent(event);
245         } else if (targ == smaller) {
246             decreaseValue(true);
247         } else if (targ == bigger) {
248             increaseValue(true);
249         } else if (DOM.eventGetType(event) == Event.MOUSEEVENTS) {
250             processBaseEvent(event);
251         } else if ((BrowserInfo.get().isGecko() && DOM.eventGetType(event) == Event.ONKEYPRESS)
252                 || (!BrowserInfo.get().isGecko() && 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)) / (double) (baseSize - handleSize))
365                     * (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 setImmediate(boolean immediate) {
513         this.immediate = immediate;
514     }
515 
516     public void setDisabled(boolean disabled) {
517         this.disabled = disabled;
518     }
519 
520     public void setReadOnly(boolean readonly) {
521         this.readonly = readonly;
522     }
523 
524     private boolean isVertical() {
525         return orientation == SliderOrientation.VERTICAL;
526     }
527 
528     public void setOrientation(SliderOrientation orientation) {
529         if (this.orientation != orientation) {
530             this.orientation = orientation;
531             updateStyleNames(getStylePrimaryName(), true);
532         }
533     }
534 
535     public void setMinValue(double value) {
536         min = value;
537     }
538 
539     public void setMaxValue(double value) {
540         max = value;
541     }
542 
543     public void setResolution(int resolution) {
544         this.resolution = resolution;
545     }
546 
547     @Override
548     public HandlerRegistration addValueChangeHandler(
549             ValueChangeHandler<Double> handler) {
550         return addHandler(handler, ValueChangeEvent.getType());
551     }
552 
553     @Override
554     public Double getValue() {
555         return value;
556     }
557 
558     @Override
559     public void setValue(Double value) {
560         if (value < min) {
561             value = min;
562         } else if (value > max) {
563             value = max;
564         }
565 
566         // Update handle position
567         final String styleAttribute = isVertical() ? "marginTop" : "marginLeft";
568         final String domProperty = isVertical() ? "offsetHeight"
569                 : "offsetWidth";
570         final int handleSize = handle.getPropertyInt(domProperty);
571         final int baseSize = base.getPropertyInt(domProperty)
572                 - (2 * BASE_BORDER_WIDTH);
573 
574         final int range = baseSize - handleSize;
575         double v = value.doubleValue();
576 
577         // Round value to resolution
578         if (resolution > 0) {
579             v = Math.round(v * Math.pow(10, resolution));
580             v = v / Math.pow(10, resolution);
581         } else {
582             v = Math.round(v);
583         }
584         final double valueRange = max - min;
585         double p = 0;
586         if (valueRange > 0) {
587             p = range * ((v - min) / valueRange);
588         }
589         if (p < 0) {
590             p = 0;
591         }
592         if (isVertical()) {
593             p = range - p;
594         }
595         final double pos = p;
596 
597         handle.getStyle().setPropertyPx(styleAttribute, (int) Math.round(pos));
598 
599         // Update value
600         this.value = new Double(v);
601     }
602 
603     @Override
604     public void setValue(Double value, boolean fireEvents) {
605         if (value == null) {
606             return;
607         }
608 
609         setValue(value);
610 
611         if (fireEvents) {
612             fireValueChanged();
613         }
614     }
615 
616     @Override
617     public com.google.gwt.user.client.Element getSubPartElement(String subPart) {
618         return null;
619     }
620 
621     @Override
622     public String getSubPartName(com.google.gwt.user.client.Element subElement) {
623         return null;
624     }
625 }