001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.gwt.client.ui;
029
030import org.opencms.gwt.client.I_CmsDescendantResizeHandler;
031import org.opencms.gwt.client.ui.css.I_CmsLayoutBundle;
032import org.opencms.gwt.client.util.CmsDebugLog;
033import org.opencms.gwt.client.util.CmsPositionBean;
034
035import com.google.gwt.dom.client.Element;
036import com.google.gwt.dom.client.Style.Display;
037import com.google.gwt.dom.client.Style.Unit;
038import com.google.gwt.event.dom.client.KeyCodes;
039import com.google.gwt.event.dom.client.ScrollEvent;
040import com.google.gwt.event.dom.client.ScrollHandler;
041import com.google.gwt.event.logical.shared.ValueChangeEvent;
042import com.google.gwt.event.logical.shared.ValueChangeHandler;
043import com.google.gwt.event.shared.HandlerRegistration;
044import com.google.gwt.user.client.DOM;
045import com.google.gwt.user.client.Event;
046import com.google.gwt.user.client.Timer;
047import com.google.gwt.user.client.Window;
048import com.google.gwt.user.client.ui.FocusPanel;
049import com.google.gwt.user.client.ui.HasValue;
050import com.google.gwt.user.client.ui.VerticalScrollbar;
051
052/**
053 * A custom scroll bar to be used with {@link org.opencms.gwt.client.ui.CmsScrollPanel}.<p>
054 */
055public class CmsScrollBar extends FocusPanel
056implements I_CmsDescendantResizeHandler, HasValue<Integer>, VerticalScrollbar {
057
058    /**
059     * The timer used to continue to shift the knob as the user holds down one of
060     * the left/right arrow keys. Only IE auto-repeats, so we just keep catching
061     * the events.
062     */
063    private class KeyTimer extends Timer {
064
065        /** A bit indicating that this is the first run. */
066        private boolean m_firstRun = true;
067
068        /** The number of steps to shift with each press. */
069        private int m_multiplier = 1;
070
071        /** The delay between shifts, which shortens as the user holds down the button. */
072        private final int m_repeatDelay = 30;
073
074        /** A bit indicating whether we are shifting to a higher or lower value. */
075        private boolean m_shiftUp;
076
077        /**
078         * Constructor.<p>
079         */
080        public KeyTimer() {
081
082            // nothing to do
083        }
084
085        /**
086         * This method will be called when a timer fires. Override it to implement
087         * the timer's logic.
088         */
089        @Override
090        public void run() {
091
092            int newPos = 0;
093            boolean stop = false;
094            // Slide the slider bar
095            if (m_shiftUp) {
096                newPos = getVerticalScrollPosition() - (m_multiplier * m_stepSize);
097            } else {
098                newPos = getVerticalScrollPosition() + (m_multiplier * m_stepSize);
099            }
100
101            // Check if we are paging while holding mouse down
102            // and make sure will not overshoot original mouse down position.
103            if (m_pagingMouse && m_shiftUp && (m_mouseDownPos > newPos)) {
104                stop = true;
105                setValue(Integer.valueOf(m_mouseDownPos));
106            } else if (m_pagingMouse && !m_shiftUp && (m_mouseDownPos < newPos)) {
107                stop = true;
108                setValue(Integer.valueOf(m_mouseDownPos));
109            }
110            if (!stop) {
111                if (m_firstRun) {
112                    m_firstRun = false;
113                }
114
115                // Slide the slider bar
116                setValue(Integer.valueOf(newPos));
117
118                // Repeat this timer until cancelled by keyup event
119                schedule(m_repeatDelay);
120            }
121        }
122
123        /**
124         * Schedules a timer to elapse in the future.
125         *
126         * @param delayMillis how long to wait before the timer elapses, in
127         *          milliseconds
128         * @param shiftUp2 whether to shift up or not
129         * @param multiplier2 the number of steps to shift
130         */
131        public void schedule(final int delayMillis, final boolean shiftUp2, final int multiplier2) {
132
133            m_firstRun = true;
134            m_shiftUp = shiftUp2;
135            m_multiplier = multiplier2;
136            super.schedule(delayMillis);
137        }
138    }
139
140    /** The initial delay. */
141    private static final int INITIALDELAY = 400;
142
143    /** The scroll knob minimum height. */
144    private static final int SCROLL_KNOB_MIN_HEIGHT = 10;
145
146    /** The scroll knob top and bottom offset. */
147    private static final int SCROLL_KNOB_OFFSET = 2;
148
149    /** The size of the increments between knob positions. */
150    protected int m_stepSize = 5;
151
152    /** The position of the first mouse down. Used for paging. */
153    int m_mouseDownPos;
154
155    /** A flag for the when the mouse button is held down when away from the slider. */
156    boolean m_pagingMouse;
157
158    /** The scroll content element. */
159    private Element m_containerElement;
160
161    /** The current scroll position. */
162    private int m_currentValue;
163
164    /**
165     * The timer used to continue to shift the knob if the user holds down a key.
166     */
167    private final KeyTimer m_keyTimer = new KeyTimer();
168
169    /** The scroll knob. */
170    private Element m_knob;
171
172    /** The current scroll knob height. */
173    private int m_knobHeight;
174
175    /** Last mouse Y position. */
176    private int m_lastMouseY;
177
178    /** Mous sliding start value. */
179    private int m_mouseSlidingStartValue;
180
181    /** Mouse sliding start Y position. */
182    private int m_mouseSlidingStartY;
183
184    /** The page size value. */
185    private int m_pageSize = 20;
186
187    /** The value knob position ratio. */
188    private double m_positionValueRatio;
189
190    /** The scrollable element. */
191    private Element m_scrollableElement;
192
193    /** A bit indicating whether or not we are currently sliding the slider bar due to keyboard events. */
194    private boolean m_slidingKeyboard;
195
196    /**
197     * A bit indicating whether or not we are currently sliding the slider bar due
198     * to mouse events.
199     */
200    private boolean m_slidingMouse;
201
202    /**
203     * Constructor.<p>
204     *
205     * @param scrollableElement the scrollable element
206     * @param containerElement the scroll content
207     */
208    public CmsScrollBar(Element scrollableElement, Element containerElement) {
209
210        I_CmsLayoutBundle.INSTANCE.scrollBarCss().ensureInjected();
211        setStyleName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().scrollBar());
212        m_scrollableElement = scrollableElement;
213        m_containerElement = containerElement;
214        m_knob = DOM.createDiv();
215        m_knob.addClassName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().scrollKnob());
216        getElement().appendChild(m_knob);
217        sinkEvents(Event.MOUSEEVENTS | Event.ONMOUSEWHEEL | Event.KEYEVENTS | Event.FOCUSEVENTS);
218    }
219
220    /**
221     * @see com.google.gwt.event.dom.client.HasScrollHandlers#addScrollHandler(com.google.gwt.event.dom.client.ScrollHandler)
222     */
223    public HandlerRegistration addScrollHandler(ScrollHandler handler) {
224
225        // Sink the event on the scrollable element, not the root element.
226        Event.sinkEvents(getScrollableElement(), Event.ONSCROLL);
227        return addHandler(handler, ScrollEvent.getType());
228    }
229
230    /**
231     * @see com.google.gwt.event.logical.shared.HasValueChangeHandlers#addValueChangeHandler(com.google.gwt.event.logical.shared.ValueChangeHandler)
232     */
233    public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Integer> handler) {
234
235        return addHandler(handler, ValueChangeEvent.getType());
236    }
237
238    /**
239     * @see com.google.gwt.user.client.ui.HasVerticalScrolling#getMaximumVerticalScrollPosition()
240     */
241    public int getMaximumVerticalScrollPosition() {
242
243        return m_containerElement.getOffsetHeight() - getScrollableElement().getOffsetHeight();
244    }
245
246    /**
247     * @see com.google.gwt.user.client.ui.HasVerticalScrolling#getMinimumVerticalScrollPosition()
248     */
249    public int getMinimumVerticalScrollPosition() {
250
251        return 0;
252    }
253
254    /**
255     * @see com.google.gwt.user.client.ui.VerticalScrollbar#getScrollHeight()
256     */
257    public int getScrollHeight() {
258
259        return m_containerElement.getOffsetHeight();
260    }
261
262    /**
263     * @see com.google.gwt.user.client.ui.HasValue#getValue()
264     */
265    public Integer getValue() {
266
267        return Integer.valueOf(m_currentValue);
268    }
269
270    /**
271     * @see com.google.gwt.user.client.ui.HasVerticalScrolling#getVerticalScrollPosition()
272     */
273    public int getVerticalScrollPosition() {
274
275        return getValue().intValue();
276    }
277
278    /**
279     * @param reziseable true if the panel is resizeable
280     *
281     */
282    public void isResizeable(boolean reziseable) {
283
284        if (reziseable) {
285            getElement().getStyle().setMarginBottom(7, Unit.PX);
286        } else {
287            getElement().getStyle().setMarginBottom(0, Unit.PX);
288        }
289    }
290
291    /**
292     * Listen for events that will move the knob.
293     *
294     * @param event the event that occurred
295     */
296    @Override
297    public final void onBrowserEvent(final Event event) {
298
299        super.onBrowserEvent(event);
300        switch (DOM.eventGetType(event)) {
301            // Unhighlight and cancel keyboard events
302            case Event.ONBLUR:
303                m_keyTimer.cancel();
304                if (m_slidingMouse) {
305                    stopMouseSliding(event);
306
307                } else if (m_slidingKeyboard) {
308                    m_slidingKeyboard = false;
309
310                }
311
312                break;
313
314            // Mousewheel events
315            case Event.ONMOUSEWHEEL:
316                int velocityY = event.getMouseWheelVelocityY() * m_stepSize;
317                event.preventDefault();
318                CmsDebugLog.getInstance().printLine("Whell velocity: " + velocityY);
319                if (velocityY > 0) {
320                    shiftDown(velocityY);
321                } else {
322                    shiftUp(-velocityY);
323                }
324                break;
325
326            // Shift left or right on key press
327            case Event.ONKEYDOWN:
328                if (!m_slidingKeyboard) {
329                    int multiplier = 1;
330                    if (event.getCtrlKey() || event.getMetaKey()) {
331                        multiplier = m_stepSize;
332                    }
333
334                    switch (event.getKeyCode()) {
335                        case KeyCodes.KEY_HOME:
336                            event.preventDefault();
337                            setValue(Integer.valueOf(0));
338                            break;
339                        case KeyCodes.KEY_END:
340                            event.preventDefault();
341                            setValue(Integer.valueOf(getMaximumVerticalScrollPosition()));
342                            break;
343                        case KeyCodes.KEY_PAGEUP:
344                            event.preventDefault();
345                            m_slidingKeyboard = true;
346                            shiftUp(m_pageSize);
347                            m_keyTimer.schedule(INITIALDELAY, true, m_pageSize);
348                            break;
349                        case KeyCodes.KEY_PAGEDOWN:
350                            event.preventDefault();
351                            m_slidingKeyboard = true;
352
353                            shiftDown(m_pageSize);
354                            m_keyTimer.schedule(INITIALDELAY, false, m_pageSize);
355                            break;
356                        case KeyCodes.KEY_UP:
357                            event.preventDefault();
358                            m_slidingKeyboard = true;
359
360                            shiftUp(multiplier);
361                            m_keyTimer.schedule(INITIALDELAY, true, multiplier);
362                            break;
363                        case KeyCodes.KEY_DOWN:
364                            event.preventDefault();
365                            m_slidingKeyboard = true;
366
367                            shiftDown(multiplier);
368                            m_keyTimer.schedule(INITIALDELAY, false, multiplier);
369                            break;
370
371                        default:
372                    }
373                }
374                break;
375            // Stop shifting on key up
376            case Event.ONKEYUP:
377                m_keyTimer.cancel();
378                if (m_slidingKeyboard) {
379                    m_slidingKeyboard = false;
380                }
381                break;
382
383            // Mouse Events
384            case Event.ONMOUSEDOWN:
385                if (sliderClicked(event)) {
386                    startMouseSliding(event);
387                    event.preventDefault();
388                }
389                break;
390            case Event.ONMOUSEUP:
391                stopMouseSliding(event);
392                break;
393            case Event.ONMOUSEMOVE:
394                slideKnob(event);
395                break;
396            default:
397        }
398
399    }
400
401    /**
402     * @see org.opencms.gwt.client.I_CmsDescendantResizeHandler#onResizeDescendant()
403     */
404    public void onResizeDescendant() {
405
406        redraw();
407    }
408
409    /**
410     * @see com.google.gwt.user.client.ui.VerticalScrollbar#setScrollHeight(int)
411     */
412    public void setScrollHeight(int height) {
413
414        redraw();
415    }
416
417    /**
418     * @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object)
419     */
420    public void setValue(Integer value) {
421
422        setValue(value, true);
423    }
424
425    /**
426     * @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object, boolean)
427     */
428    public void setValue(Integer value, boolean fireEvents) {
429
430        if (value != null) {
431            m_currentValue = value.intValue();
432        } else {
433            m_currentValue = 0;
434        }
435        setKnobPosition(m_currentValue);
436        // Fire the ValueChangeEvent
437        if (fireEvents) {
438            ValueChangeEvent.fire(this, Integer.valueOf(m_currentValue));
439        }
440    }
441
442    /**
443     * @see com.google.gwt.user.client.ui.HasVerticalScrolling#setVerticalScrollPosition(int)
444     */
445    public void setVerticalScrollPosition(int position) {
446
447        setValue(Integer.valueOf(position));
448    }
449
450    /**
451     * Returns the associated scrollable element.<p>
452     *
453     * @return the associated scrollable element
454     */
455    protected Element getScrollableElement() {
456
457        return m_scrollableElement;
458    }
459
460    /**
461     * @see com.google.gwt.user.client.ui.Widget#onAttach()
462     */
463    @Override
464    protected void onAttach() {
465
466        super.onAttach();
467
468        /*
469         * Attach the event listener in onAttach instead of onLoad so users cannot
470         * accidentally override it.
471         */
472        Event.setEventListener(getScrollableElement(), this);
473        redraw();
474    }
475
476    /**
477     * @see com.google.gwt.user.client.ui.Widget#onDetach()
478     */
479    @Override
480    protected void onDetach() {
481
482        /*
483         * Detach the event listener in onDetach instead of onUnload so users cannot
484         * accidentally override it.
485         */
486        Event.setEventListener(getScrollableElement(), null);
487
488        super.onDetach();
489    }
490
491    /**
492     * Redraws the scroll bar.<p>
493     */
494    protected void redraw() {
495
496        if (isAttached()) {
497            int outerHeight = getElement().getOffsetHeight();
498            int innerHeight = m_containerElement.getOffsetHeight();
499            if (outerHeight >= innerHeight) {
500                setScrollbarVisible(false);
501            } else {
502                setScrollbarVisible(true);
503                adjustKnobHeight(outerHeight, innerHeight);
504                setKnobPosition(m_currentValue);
505            }
506        }
507        if (m_slidingMouse) {
508            m_mouseSlidingStartY = m_lastMouseY;
509            m_mouseSlidingStartValue = m_currentValue;
510        }
511    }
512
513    /**
514     * Shifts the scroll position down.<p>
515     *
516     * @param shift the shift size
517     */
518    protected void shiftDown(int shift) {
519
520        int max = getMaximumVerticalScrollPosition();
521        if ((m_currentValue + shift) < max) {
522            setVerticalScrollPosition(m_currentValue + shift);
523        } else {
524            setVerticalScrollPosition(max);
525        }
526    }
527
528    /**
529     * Shifts the scroll position up.<p>
530     *
531     * @param shift the shift size
532     */
533    protected void shiftUp(int shift) {
534
535        int min = getMinimumVerticalScrollPosition();
536        if ((m_currentValue - shift) > min) {
537            setVerticalScrollPosition(m_currentValue - shift);
538        } else {
539            setVerticalScrollPosition(min);
540        }
541    }
542
543    /**
544     * Calculates the scroll knob height.<p>
545     *
546     * @param outerHeight the height of the scrollable element
547     * @param innerHeight the height of the scroll content
548     */
549    private void adjustKnobHeight(int outerHeight, int innerHeight) {
550
551        int result = (int)((1.0 * outerHeight * outerHeight) / innerHeight);
552        result = result > (outerHeight - 5) ? 5 : (result < 8 ? 8 : result);
553        m_positionValueRatio = (1.0 * (outerHeight - result)) / (innerHeight - outerHeight);
554        m_knobHeight = result - (2 * SCROLL_KNOB_OFFSET);
555        m_knobHeight = m_knobHeight < SCROLL_KNOB_MIN_HEIGHT ? SCROLL_KNOB_MIN_HEIGHT : m_knobHeight;
556        m_knob.getStyle().setHeight(m_knobHeight, Unit.PX);
557    }
558
559    /**
560     * Sets the scroll knob position according to the given value.<p>
561     *
562     * @param value the value
563     */
564    private void setKnobPosition(int value) {
565
566        int top = (int)(SCROLL_KNOB_OFFSET + (m_positionValueRatio * value));
567        int maxPosition = getElement().getOffsetHeight() - m_knobHeight - SCROLL_KNOB_OFFSET;
568        top = top < SCROLL_KNOB_OFFSET ? SCROLL_KNOB_OFFSET : (top > maxPosition ? maxPosition : top);
569        m_knob.getStyle().setTop(top, Unit.PX);
570    }
571
572    /**
573     * Sets the scroll bar visibility.<p>
574     *
575     * @param visible <code>true</code> to set the scroll bar visible
576     */
577    private void setScrollbarVisible(boolean visible) {
578
579        if (visible) {
580            getElement().getStyle().clearWidth();
581            m_knob.getStyle().clearDisplay();
582        } else {
583            getElement().getStyle().setWidth(0, Unit.PX);
584            m_knob.getStyle().setDisplay(Display.NONE);
585        }
586    }
587
588    /**
589     * Sides the scroll knob according to the mouse event.<p>
590     *
591     * @param event the mouse event
592     */
593    private void slideKnob(Event event) {
594
595        if (m_slidingMouse) {
596            m_lastMouseY = event.getClientY();
597            int shift = (int)((m_lastMouseY - m_mouseSlidingStartY) / m_positionValueRatio);
598            int nextValue = m_mouseSlidingStartValue + shift;
599            CmsDebugLog.getInstance().printLine("Mouse sliding should set value to: " + nextValue);
600            int max = getMaximumVerticalScrollPosition();
601            int min = getMinimumVerticalScrollPosition();
602            if (nextValue < min) {
603                nextValue = min;
604            } else if (nextValue > max) {
605                nextValue = max;
606            }
607            setValue(Integer.valueOf(nextValue));
608        }
609    }
610
611    /**
612     * Returns <code>true</code> if the events mouse position is above the scroll bar knob.<p>
613     *
614     * @param event the mouse event
615     *
616     * @return <code>true</code> if the events mouse position is above the scroll bar knob
617     */
618    private boolean sliderClicked(Event event) {
619
620        boolean result = CmsPositionBean.generatePositionInfo(m_knob).isOverElement(
621            event.getClientX() + Window.getScrollLeft(),
622            event.getClientY() + Window.getScrollTop());
623        CmsDebugLog.getInstance().printLine("Slider was clicked: " + result);
624        return result;
625    }
626
627    /**
628     * Starts the mouse sliding.<p>
629     *
630     * @param event the mouse event
631     */
632    private void startMouseSliding(Event event) {
633
634        if (!m_slidingMouse) {
635            m_slidingMouse = true;
636            DOM.setCapture(getElement());
637            m_mouseSlidingStartY = event.getClientY();
638            m_mouseSlidingStartValue = m_currentValue;
639            CmsDebugLog.getInstance().printLine(
640                "Mouse sliding started with clientY: "
641                    + m_mouseSlidingStartY
642                    + " and start value: "
643                    + m_mouseSlidingStartValue
644                    + " and a max value of "
645                    + getMaximumVerticalScrollPosition());
646        }
647    }
648
649    /**
650     * Stops the mouse sliding.<p>
651     *
652     * @param event the mouse event
653     */
654    private void stopMouseSliding(Event event) {
655
656        slideKnob(event);
657        m_slidingMouse = false;
658        DOM.releaseCapture(getElement());
659    }
660}