All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.v7.client.ui.VSlider Maven / Gradle / Ivy

There is a newer version: 8.27.3
Show newest version
/*
 * Copyright (C) 2000-2024 Vaadin Ltd
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See  for the full
 * license.
 */
package com.vaadin.v7.client.ui;

import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasValue;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.Field;
import com.vaadin.client.ui.SimpleFocusablePanel;
import com.vaadin.client.ui.SubPartAware;
import com.vaadin.client.ui.VLazyExecutor;
import com.vaadin.client.ui.VOverlay;
import com.vaadin.v7.shared.ui.slider.SliderOrientation;

public class VSlider extends SimpleFocusablePanel
        implements Field, HasValue, SubPartAware {

    public static final String CLASSNAME = "v-slider";

    /**
     * Minimum size (width or height, depending on orientation) of the slider
     * base.
     */
    private static final int MIN_SIZE = 50;

    protected ApplicationConnection client;

    protected String id;

    protected boolean immediate;
    protected boolean disabled;
    protected boolean readonly;

    private int acceleration = 1;
    protected double min;
    protected double max;
    protected int resolution;
    protected Double value;
    protected SliderOrientation orientation = SliderOrientation.HORIZONTAL;

    private final HTML feedback = new HTML("", false);
    private final VOverlay feedbackPopup = new VOverlay(true, false) {
        {
            setOwner(VSlider.this);
        }

        @Override
        public void show() {
            super.show();
            updateFeedbackPosition();
        }
    };

    /* DOM element for slider's base */
    private final Element base;
    private static final int BASE_BORDER_WIDTH = 1;

    /* DOM element for slider's handle */
    private final Element handle;

    /* DOM element for decrement arrow */
    private final Element smaller;

    /* DOM element for increment arrow */
    private final Element bigger;

    /* Temporary dragging/animation variables */
    private boolean dragging = false;

    private VLazyExecutor delayedValueUpdater = new VLazyExecutor(100,
            new ScheduledCommand() {

                @Override
                public void execute() {
                    fireValueChanged();
                    acceleration = 1;
                }
            });

    public VSlider() {
        super();

        base = DOM.createDiv();
        handle = DOM.createDiv();
        smaller = DOM.createDiv();
        bigger = DOM.createDiv();

        setStyleName(CLASSNAME);

        getElement().appendChild(bigger);
        getElement().appendChild(smaller);
        getElement().appendChild(base);
        base.appendChild(handle);

        // Hide initially
        smaller.getStyle().setDisplay(Display.NONE);
        bigger.getStyle().setDisplay(Display.NONE);

        sinkEvents(Event.MOUSEEVENTS | Event.ONMOUSEWHEEL | Event.KEYEVENTS
                | Event.FOCUSEVENTS | Event.TOUCHEVENTS);

        feedbackPopup.setWidget(feedback);
    }

    @Override
    public void setStyleName(String style) {
        updateStyleNames(style, false);
    }

    @Override
    public void setStylePrimaryName(String style) {
        updateStyleNames(style, true);
    }

    protected void updateStyleNames(String styleName,
            boolean isPrimaryStyleName) {

        feedbackPopup.removeStyleName(getStylePrimaryName() + "-feedback");
        removeStyleName(getStylePrimaryName() + "-vertical");

        if (isPrimaryStyleName) {
            super.setStylePrimaryName(styleName);
        } else {
            super.setStyleName(styleName);
        }

        feedbackPopup.addStyleName(getStylePrimaryName() + "-feedback");
        base.setClassName(getStylePrimaryName() + "-base");
        handle.setClassName(getStylePrimaryName() + "-handle");
        smaller.setClassName(getStylePrimaryName() + "-smaller");
        bigger.setClassName(getStylePrimaryName() + "-bigger");

        if (isVertical()) {
            addStyleName(getStylePrimaryName() + "-vertical");
        }
    }

    public void setFeedbackValue(double value) {
        feedback.setText(String.valueOf(value));
    }

    private void updateFeedbackPosition() {
        if (isVertical()) {
            feedbackPopup.setPopupPosition(
                    handle.getAbsoluteLeft() + handle.getOffsetWidth(),
                    handle.getAbsoluteTop() + handle.getOffsetHeight() / 2
                            - feedbackPopup.getOffsetHeight() / 2);
        } else {
            feedbackPopup.setPopupPosition(
                    handle.getAbsoluteLeft() + handle.getOffsetWidth() / 2
                            - feedbackPopup.getOffsetWidth() / 2,
                    handle.getAbsoluteTop() - feedbackPopup.getOffsetHeight());
        }
    }

    /** For internal use only. May be removed or replaced in the future. */
    public void buildBase() {
        final String styleAttribute = isVertical() ? "height" : "width";
        final String oppositeStyleAttribute = isVertical() ? "width" : "height";
        final String domProperty = isVertical() ? "offsetHeight"
                : "offsetWidth";

        // clear unnecessary opposite style attribute
        base.getStyle().clearProperty(oppositeStyleAttribute);

        /*
         * To resolve defect #13681 we should not return from method buildBase()
         * if slider has no parentElement, because such operations as
         * buildHandle() and setValues(), which are needed for Slider, are
         * called at the end of method buildBase(). And these methods will not
         * be called if there is no parentElement. So, instead of returning from
         * method buildBase() if there is no parentElement "if condition" is
         * applied to call code for parentElement only in case it exists.
         */
        if (getElement().hasParentElement()) {
            final Element p = getElement();
            if (p.getPropertyInt(domProperty) > MIN_SIZE) {
                if (isVertical()) {
                    setHeight();
                } else {
                    base.getStyle().clearProperty(styleAttribute);
                }
            } else {
                // Set minimum size and adjust after all components have
                // (supposedly) been drawn completely.
                base.getStyle().setPropertyPx(styleAttribute, MIN_SIZE);
                Scheduler.get().scheduleDeferred(new Command() {

                    @Override
                    public void execute() {
                        final Element p = getElement();
                        if (p.getPropertyInt(domProperty) > MIN_SIZE + 5
                                || propertyNotNullOrEmpty(styleAttribute, p)) {
                            if (isVertical()) {
                                setHeight();
                            } else {
                                base.getStyle().clearProperty(styleAttribute);
                            }
                            // Ensure correct position
                            setValue(value, false);
                        }
                    }

                    // Style has non empty property
                    private boolean propertyNotNullOrEmpty(
                            final String styleAttribute, final Element p) {
                        return p.getStyle().getProperty(styleAttribute) != null
                                && !p.getStyle().getProperty(styleAttribute)
                                        .isEmpty();
                    }
                });
            }
        }

        if (!isVertical()) {
            // Draw handle with a delay to allow base to gain maximum width
            Scheduler.get().scheduleDeferred(new Command() {
                @Override
                public void execute() {
                    buildHandle();
                    setValue(value, false);
                }
            });
        } else {
            buildHandle();
            setValue(value, false);
        }

        // TODO attach listeners for focusing and arrow keys
    }

    void buildHandle() {
        final String handleAttribute = isVertical() ? "marginTop"
                : "marginLeft";
        final String oppositeHandleAttribute = isVertical() ? "marginLeft"
                : "marginTop";

        handle.getStyle().setProperty(handleAttribute, "0");

        // clear unnecessary opposite handle attribute
        handle.getStyle().clearProperty(oppositeHandleAttribute);
    }

    @Override
    public void onBrowserEvent(Event event) {
        if (disabled || readonly) {
            return;
        }
        final Element targ = DOM.eventGetTarget(event);

        if (DOM.eventGetType(event) == Event.ONMOUSEWHEEL) {
            processMouseWheelEvent(event);
        } else if (dragging || targ == handle) {
            processHandleEvent(event);
        } else if (targ == smaller) {
            decreaseValue(true);
        } else if (targ == bigger) {
            increaseValue(true);
        } else if (DOM.eventGetType(event) == Event.MOUSEEVENTS) {
            processBaseEvent(event);
        } else if (isNavigationEvent(event)) {

            if (handleNavigation(event.getKeyCode(), event.getCtrlKey(),
                    event.getShiftKey())) {

                feedbackPopup.show();

                delayedValueUpdater.trigger();

                DOM.eventPreventDefault(event);
                DOM.eventCancelBubble(event, true);
            }
        } else if (targ.equals(getElement())
                && DOM.eventGetType(event) == Event.ONFOCUS) {
            feedbackPopup.show();
        } else if (targ.equals(getElement())
                && DOM.eventGetType(event) == Event.ONBLUR) {
            feedbackPopup.hide();
        } else if (DOM.eventGetType(event) == Event.ONMOUSEDOWN) {
            feedbackPopup.show();
        }
        if (WidgetUtil.isTouchEvent(event)) {
            event.preventDefault(); // avoid simulated events
            event.stopPropagation();
        }
    }

    private boolean isNavigationEvent(Event event) {
        if (BrowserInfo.get().isGecko() && BrowserInfo.get().getGeckoVersion() < 65) {
            return DOM.eventGetType(event) == Event.ONKEYPRESS;
        } else {
            return DOM.eventGetType(event) == Event.ONKEYDOWN;
        }
    }

    private void processMouseWheelEvent(final Event event) {
        final int dir = DOM.eventGetMouseWheelVelocityY(event);

        if (dir < 0) {
            increaseValue(false);
        } else {
            decreaseValue(false);
        }

        delayedValueUpdater.trigger();

        DOM.eventPreventDefault(event);
        DOM.eventCancelBubble(event, true);
    }

    private void processHandleEvent(Event event) {
        switch (DOM.eventGetType(event)) {
        case Event.ONMOUSEDOWN:
        case Event.ONTOUCHSTART:
            if (!disabled && !readonly) {
                focus();
                feedbackPopup.show();
                dragging = true;
                handle.setClassName(getStylePrimaryName() + "-handle");
                handle.addClassName(getStylePrimaryName() + "-handle-active");

                DOM.setCapture(getElement());
                DOM.eventPreventDefault(event); // prevent selecting text
                DOM.eventCancelBubble(event, true);
                event.stopPropagation();
            }
            break;
        case Event.ONMOUSEMOVE:
        case Event.ONTOUCHMOVE:
            if (dragging) {
                setValueByEvent(event, false);
                updateFeedbackPosition();
                event.stopPropagation();
            }
            break;
        case Event.ONTOUCHEND:
            feedbackPopup.hide();
        case Event.ONMOUSEUP:
            // feedbackPopup.hide();
            dragging = false;
            handle.setClassName(getStylePrimaryName() + "-handle");
            DOM.releaseCapture(getElement());
            setValueByEvent(event, true);
            event.stopPropagation();
            break;
        default:
            break;
        }
    }

    private void processBaseEvent(Event event) {
        if (DOM.eventGetType(event) == Event.ONMOUSEDOWN) {
            if (!disabled && !readonly && !dragging) {
                setValueByEvent(event, true);
                DOM.eventCancelBubble(event, true);
            }
        }
    }

    private void decreaseValue(boolean updateToServer) {
        setValue(new Double(value.doubleValue() - Math.pow(10, -resolution)),
                updateToServer);
    }

    private void increaseValue(boolean updateToServer) {
        setValue(new Double(value.doubleValue() + Math.pow(10, -resolution)),
                updateToServer);
    }

    private void setValueByEvent(Event event, boolean updateToServer) {
        double v = min; // Fallback to min

        final int coord = getEventPosition(event);

        final int handleSize, baseSize, baseOffset;
        if (isVertical()) {
            handleSize = handle.getOffsetHeight();
            baseSize = base.getOffsetHeight();
            baseOffset = base.getAbsoluteTop() - Window.getScrollTop()
                    - handleSize / 2;
        } else {
            handleSize = handle.getOffsetWidth();
            baseSize = base.getOffsetWidth();
            baseOffset = base.getAbsoluteLeft() - Window.getScrollLeft()
                    + handleSize / 2;
        }

        if (isVertical()) {
            v = (baseSize - (coord - baseOffset))
                    / (double) (baseSize - handleSize) * (max - min) + min;
        } else {
            v = (coord - baseOffset) / (double) (baseSize - handleSize)
                    * (max - min) + min;
        }

        if (v < min) {
            v = min;
        } else if (v > max) {
            v = max;
        }

        setValue(v, updateToServer);
    }

    /**
     * TODO consider extracting touches support to an impl class specific for
     * webkit (only browser that really supports touches).
     *
     * @param event
     * @return
     */
    protected int getEventPosition(Event event) {
        if (isVertical()) {
            return WidgetUtil.getTouchOrMouseClientY(event);
        } else {
            return WidgetUtil.getTouchOrMouseClientX(event);
        }
    }

    public void iLayout() {
        if (isVertical()) {
            setHeight();
        }
        // Update handle position
        setValue(value, false);
    }

    private void setHeight() {
        // Calculate decoration size
        base.getStyle().setHeight(0, Unit.PX);
        base.getStyle().setOverflow(Overflow.HIDDEN);
        int h = getElement().getOffsetHeight();
        if (h < MIN_SIZE) {
            h = MIN_SIZE;
        }
        base.getStyle().setHeight(h, Unit.PX);
        base.getStyle().clearOverflow();
    }

    private void fireValueChanged() {
        ValueChangeEvent.fire(VSlider.this, value);
    }

    /**
     * Handles the keyboard events handled by the Slider.
     *
     * @param keycode
     *            The key code received
     * @param ctrl
     *            Whether {@code CTRL} was pressed
     * @param shift
     *            Whether {@code SHIFT} was pressed
     * @return true if the navigation event was handled
     */
    public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {

        // No support for ctrl moving
        if (ctrl) {
            return false;
        }

        if (keycode == getNavigationUpKey() && isVertical()
                || keycode == getNavigationRightKey() && !isVertical()) {
            if (shift) {
                for (int a = 0; a < acceleration; a++) {
                    increaseValue(false);
                }
                acceleration++;
            } else {
                increaseValue(false);
            }
            return true;
        } else if (keycode == getNavigationDownKey() && isVertical()
                || keycode == getNavigationLeftKey() && !isVertical()) {
            if (shift) {
                for (int a = 0; a < acceleration; a++) {
                    decreaseValue(false);
                }
                acceleration++;
            } else {
                decreaseValue(false);
            }
            return true;
        }

        return false;
    }

    /**
     * Get the key that increases the vertical slider. By default it is the up
     * arrow key but by overriding this you can change the key to whatever you
     * want.
     *
     * @return The keycode of the key
     */
    protected int getNavigationUpKey() {
        return KeyCodes.KEY_UP;
    }

    /**
     * Get the key that decreases the vertical slider. By default it is the down
     * arrow key but by overriding this you can change the key to whatever you
     * want.
     *
     * @return The keycode of the key
     */
    protected int getNavigationDownKey() {
        return KeyCodes.KEY_DOWN;
    }

    /**
     * Get the key that decreases the horizontal slider. By default it is the
     * left arrow key but by overriding this you can change the key to whatever
     * you want.
     *
     * @return The keycode of the key
     */
    protected int getNavigationLeftKey() {
        return KeyCodes.KEY_LEFT;
    }

    /**
     * Get the key that increases the horizontal slider. By default it is the
     * right arrow key but by overriding this you can change the key to whatever
     * you want.
     *
     * @return The keycode of the key
     */
    protected int getNavigationRightKey() {
        return KeyCodes.KEY_RIGHT;
    }

    public void setConnection(ApplicationConnection client) {
        this.client = client;
    }

    public void setId(String id) {
        this.id = id;
    }

    public void setImmediate(boolean immediate) {
        this.immediate = immediate;
    }

    public void setDisabled(boolean disabled) {
        this.disabled = disabled;
    }

    public void setReadOnly(boolean readonly) {
        this.readonly = readonly;
    }

    private boolean isVertical() {
        return orientation == SliderOrientation.VERTICAL;
    }

    public void setOrientation(SliderOrientation orientation) {
        if (this.orientation != orientation) {
            this.orientation = orientation;
            updateStyleNames(getStylePrimaryName(), true);
        }
    }

    public void setMinValue(double value) {
        min = value;
    }

    public void setMaxValue(double value) {
        max = value;
    }

    public void setResolution(int resolution) {
        this.resolution = resolution;
    }

    @Override
    public HandlerRegistration addValueChangeHandler(
            ValueChangeHandler handler) {
        return addHandler(handler, ValueChangeEvent.getType());
    }

    @Override
    public Double getValue() {
        return value;
    }

    @Override
    public void setValue(Double value) {
        if (value < min) {
            value = min;
        } else if (value > max) {
            value = max;
        }

        // Update handle position
        final String styleAttribute = isVertical() ? "marginTop" : "marginLeft";
        final String domProperty = isVertical() ? "offsetHeight"
                : "offsetWidth";
        final int handleSize = handle.getPropertyInt(domProperty);
        final int baseSize = base.getPropertyInt(domProperty)
                - 2 * BASE_BORDER_WIDTH;

        final int range = baseSize - handleSize;
        double v = value.doubleValue();

        // Round value to resolution
        if (resolution > 0) {
            v = Math.round(v * Math.pow(10, resolution));
            v = v / Math.pow(10, resolution);
        } else {
            v = Math.round(v);
        }
        final double valueRange = max - min;
        double p = 0;
        if (valueRange > 0) {
            p = range * ((v - min) / valueRange);
        }
        if (p < 0) {
            p = 0;
        }
        if (isVertical()) {
            p = range - p;
        }
        final double pos = p;

        handle.getStyle().setPropertyPx(styleAttribute, (int) Math.round(pos));

        // Update value
        this.value = new Double(v);
        setFeedbackValue(v);
    }

    @Override
    public void setValue(Double value, boolean fireEvents) {
        if (value == null) {
            return;
        }

        setValue(value);

        if (fireEvents) {
            fireValueChanged();
        }
    }

    @Override
    public com.google.gwt.user.client.Element getSubPartElement(
            String subPart) {
        if (subPart.equals("popup")) {
            feedbackPopup.show();
            return feedbackPopup.getElement();
        }
        return null;
    }

    @Override
    public String getSubPartName(
            com.google.gwt.user.client.Element subElement) {
        if (feedbackPopup.getElement().isOrHasChild(subElement)) {
            return "popup";
        }
        return null;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy