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.CmsDomUtil;
033import org.opencms.gwt.client.util.CmsDomUtil.Style;
034import org.opencms.gwt.client.util.CmsFadeAnimation;
035
036import java.util.ArrayList;
037import java.util.Iterator;
038import java.util.List;
039
040import com.google.gwt.core.client.Scheduler;
041import com.google.gwt.core.client.Scheduler.ScheduledCommand;
042import com.google.gwt.dom.client.Element;
043import com.google.gwt.dom.client.Style.Display;
044import com.google.gwt.dom.client.Style.Overflow;
045import com.google.gwt.dom.client.Style.Unit;
046import com.google.gwt.event.dom.client.MouseOutEvent;
047import com.google.gwt.event.dom.client.MouseOutHandler;
048import com.google.gwt.event.dom.client.MouseOverEvent;
049import com.google.gwt.event.dom.client.MouseOverHandler;
050import com.google.gwt.event.logical.shared.ValueChangeEvent;
051import com.google.gwt.event.logical.shared.ValueChangeHandler;
052import com.google.gwt.event.shared.HandlerRegistration;
053import com.google.gwt.user.client.Command;
054import com.google.gwt.user.client.DOM;
055import com.google.gwt.user.client.Event;
056import com.google.gwt.user.client.Timer;
057import com.google.gwt.user.client.ui.AbstractNativeScrollbar;
058import com.google.gwt.user.client.ui.VerticalScrollbar;
059import com.google.gwt.user.client.ui.Widget;
060
061/**
062 * Scroll panel implementation with custom scroll bars. Works in all browsers but IE7.<p>
063 */
064public class CmsScrollPanelImpl extends CmsScrollPanel {
065
066    /**
067     * Handler to show and hide the scroll bar on hover.<p>
068     */
069    private class HoverHandler implements MouseOutHandler, MouseOverHandler {
070
071        /** The owner element. */
072        Element m_owner;
073
074        /** The element to fade in and out. */
075        private Element m_fadeElement;
076
077        /** The currently running hide animation. */
078        private CmsFadeAnimation m_hideAnimation;
079
080        /** The timer to hide the scroll bar with a delay. */
081        private Timer m_removeTimer;
082
083        /**
084         * Constructor.<p>
085         *
086         * @param owner the owner element
087         * @param fadeElement the element to fade in and out on hover
088         */
089        HoverHandler(Element owner, Element fadeElement) {
090
091            m_owner = owner;
092            m_fadeElement = fadeElement;
093        }
094
095        /**
096         * @see com.google.gwt.event.dom.client.MouseOutHandler#onMouseOut(com.google.gwt.event.dom.client.MouseOutEvent)
097         */
098        public void onMouseOut(MouseOutEvent event) {
099
100            m_removeTimer = new Timer() {
101
102                /**
103                 * @see com.google.gwt.user.client.Timer#run()
104                 */
105                @Override
106                public void run() {
107
108                    clearShowing();
109                }
110            };
111            m_removeTimer.schedule(1000);
112        }
113
114        /**
115         * @see com.google.gwt.event.dom.client.MouseOverHandler#onMouseOver(com.google.gwt.event.dom.client.MouseOverEvent)
116         */
117        public void onMouseOver(MouseOverEvent event) {
118
119            if ((m_hideAnimation != null)
120                || !CmsDomUtil.hasClass(I_CmsLayoutBundle.INSTANCE.scrollBarCss().showBars(), m_owner)) {
121                if (m_hideAnimation != null) {
122                    m_hideAnimation.cancel();
123                    m_hideAnimation = null;
124                } else {
125                    CmsFadeAnimation.fadeIn(m_fadeElement, null, 100);
126                }
127                m_owner.addClassName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().showBars());
128            }
129            if (m_removeTimer != null) {
130                m_removeTimer.cancel();
131                m_removeTimer = null;
132            }
133        }
134
135        /**
136         * Hides the scroll bar.<p>
137         */
138        void clearShowing() {
139
140            m_hideAnimation = CmsFadeAnimation.fadeOut(m_fadeElement, new Command() {
141
142                public void execute() {
143
144                    m_owner.removeClassName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().showBars());
145
146                }
147            }, 200);
148
149            m_removeTimer = null;
150        }
151    }
152
153    /** Hidden element to measure the appropriate size of the container element. */
154    private Element m_hiddenSize;
155
156    /** The measured width of the native scroll bars. */
157    private int m_nativeScrollbarWidth;
158
159    /** The vertical scroll bar. */
160    private VerticalScrollbar m_scrollbar;
161
162    /** The scroll layer. */
163    private Element m_scrollLayer;
164
165    /** The scroll bar change handler registration. */
166    private HandlerRegistration m_verticalScrollbarHandlerRegistration;
167
168    /** The scroll bar width. */
169    private int m_verticalScrollbarWidth;
170
171    /**
172     * Constructor.<p>
173     */
174    public CmsScrollPanelImpl() {
175
176        super(DOM.createDiv(), DOM.createDiv(), DOM.createDiv());
177        setStyleName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().scrollPanel());
178        Element scrollable = getScrollableElement();
179        scrollable.getStyle().clearPosition();
180        scrollable.getStyle().setOverflowX(Overflow.HIDDEN);
181        scrollable.setClassName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().scrollable());
182        getElement().appendChild(scrollable);
183        Element container = getContainerElement();
184        container.setClassName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().scrollContainer());
185        scrollable.appendChild(container);
186        m_scrollLayer = DOM.createDiv();
187        getElement().appendChild(m_scrollLayer);
188        m_scrollLayer.setClassName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().scrollbarLayer());
189        CmsScrollBar scrollbar = new CmsScrollBar(scrollable, container);
190        setVerticalScrollbar(scrollbar, 8);
191        m_hiddenSize = DOM.createDiv();
192        m_hiddenSize.setClassName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().hiddenSize());
193
194        /*
195         * Listen for scroll events from the root element and the scrollable element
196         * so we can align the scrollbars with the content. Scroll events usually
197         * come from the scrollable element, but they can also come from the root
198         * element if the user clicks and drags the content, which reveals the
199         * hidden scrollbars.
200         */
201        Event.sinkEvents(getElement(), Event.ONSCROLL);
202        Event.sinkEvents(scrollable, Event.ONSCROLL);
203        initHoverHandler();
204    }
205
206    /**
207     * @see com.google.gwt.user.client.ui.SimplePanel#iterator()
208     */
209    @Override
210    public Iterator<Widget> iterator() {
211
212        // Return a simple iterator that enumerates the 0 or 1 elements in this
213        // panel.
214        List<Widget> widgets = new ArrayList<Widget>();
215        if (getWidget() != null) {
216            widgets.add(getWidget());
217        }
218        if (getVerticalScrollBar() != null) {
219            widgets.add(getVerticalScrollBar().asWidget());
220        }
221        final Iterator<Widget> internalIterator = widgets.iterator();
222        return new Iterator<Widget>() {
223
224            public boolean hasNext() {
225
226                return internalIterator.hasNext();
227            }
228
229            public Widget next() {
230
231                return internalIterator.next();
232            }
233
234            @Override
235            public void remove() {
236
237                throw new UnsupportedOperationException();
238            }
239        };
240    }
241
242    /**
243     * @see com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user.client.Event)
244     */
245    @Override
246    public void onBrowserEvent(Event event) {
247
248        // Align the scrollbars with the content.
249        if (Event.ONSCROLL == event.getTypeInt()) {
250            maybeUpdateScrollbarPositions();
251        }
252        super.onBrowserEvent(event);
253    }
254
255    /**
256     * @see org.opencms.gwt.client.ui.CmsScrollPanel#onResizeDescendant()
257     */
258    @Override
259    public void onResizeDescendant() {
260
261        int maxHeight = CmsDomUtil.getCurrentStyleInt(getElement(), Style.maxHeight);
262        if (maxHeight > 0) {
263            getScrollableElement().getStyle().setPropertyPx("maxHeight", maxHeight);
264        }
265        // appending div to measure panel width, doing it every time anew to avoid rendering bugs in Chrome
266        getElement().appendChild(m_hiddenSize);
267        int width = m_hiddenSize.getClientWidth();
268        m_hiddenSize.removeFromParent();
269        if (width > 0) {
270            getContainerElement().getStyle().setWidth(width, Unit.PX);
271            maybeUpdateScrollbars();
272        }
273    }
274
275    /**
276     * @see org.opencms.gwt.client.ui.CmsScrollPanel#setResizable(boolean)
277     */
278    @Override
279    public void setResizable(boolean resize) {
280
281        super.setResizable(resize);
282        if (resize) {
283            m_scrollbar.asWidget().getElement().getStyle().setMarginBottom(7, Unit.PX);
284        } else {
285            m_scrollbar.asWidget().getElement().getStyle().setMarginBottom(0, Unit.PX);
286        }
287    }
288
289    /**
290     * Returns the vertical scroll bar.<p>
291     *
292     * @return the vertical scroll bar
293     */
294    protected VerticalScrollbar getVerticalScrollBar() {
295
296        return m_scrollbar;
297    }
298
299    /**
300     * @see com.google.gwt.user.client.ui.ScrollPanel#onAttach()
301     */
302    @Override
303    protected void onAttach() {
304
305        super.onAttach();
306        hideNativeScrollbars();
307        onResizeDescendant();
308    }
309
310    /**
311     * @see com.google.gwt.user.client.ui.Widget#onLoad()
312     */
313    @Override
314    protected void onLoad() {
315
316        super.onLoad();
317        hideNativeScrollbars();
318        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
319
320            public void execute() {
321
322                onResizeDescendant();
323            }
324        });
325    }
326
327    /**
328     * Hide the native scrollbars. We call this after attaching to ensure that we
329     * inherit the direction (rtl or ltr).
330     */
331    private void hideNativeScrollbars() {
332
333        m_nativeScrollbarWidth = AbstractNativeScrollbar.getNativeScrollbarWidth();
334        getScrollableElement().getStyle().setMarginRight(-(m_nativeScrollbarWidth + 10), Unit.PX);
335    }
336
337    /**
338     * Initializes the hover handler to hide and show the scroll bar on hover.<p>
339     */
340    private void initHoverHandler() {
341
342        HoverHandler handler = new HoverHandler(getElement(), m_scrollbar.asWidget().getElement());
343        addDomHandler(handler, MouseOverEvent.getType());
344        addDomHandler(handler, MouseOutEvent.getType());
345    }
346
347    /**
348     * Synchronize the scroll positions of the scrollbars with the actual scroll
349     * position of the content.
350     */
351    private void maybeUpdateScrollbarPositions() {
352
353        if (!isAttached()) {
354            return;
355        }
356
357        if (m_scrollbar != null) {
358            int vPos = getVerticalScrollPosition();
359            if (m_scrollbar.getVerticalScrollPosition() != vPos) {
360                m_scrollbar.setVerticalScrollPosition(vPos);
361            }
362        }
363    }
364
365    /**
366     * Update the position of the scrollbars.<p>
367     * If only the vertical scrollbar is present, it takes up the entire height of
368     * the right side. If only the horizontal scrollbar is present, it takes up
369     * the entire width of the bottom. If both scrollbars are present, the
370     * vertical scrollbar extends from the top to just above the horizontal
371     * scrollbar, and the horizontal scrollbar extends from the left to just right
372     * of the vertical scrollbar, leaving a small square in the bottom right
373     * corner.<p>
374     */
375    private void maybeUpdateScrollbars() {
376
377        if (!isAttached()) {
378            return;
379        }
380
381        /*
382         * Measure the height and width of the content directly. Note that measuring
383         * the height and width of the container element (which should be the same)
384         * doesn't work correctly in IE.
385         */
386        Widget w = getWidget();
387        int contentHeight = (w == null) ? 0 : w.getOffsetHeight();
388
389        // Determine which scrollbars to show.
390        int realScrollbarHeight = 0;
391        int realScrollbarWidth = 0;
392        if ((m_scrollbar != null) && (getElement().getClientHeight() < contentHeight)) {
393            // Vertical scrollbar is defined and required.
394            realScrollbarWidth = m_verticalScrollbarWidth;
395        }
396
397        if (realScrollbarWidth > 0) {
398            m_scrollLayer.getStyle().clearDisplay();
399
400            m_scrollbar.setScrollHeight(Math.max(0, contentHeight - realScrollbarHeight));
401        } else if (m_scrollLayer != null) {
402            m_scrollLayer.getStyle().setDisplay(Display.NONE);
403        }
404        if (m_scrollbar instanceof I_CmsDescendantResizeHandler) {
405            ((I_CmsDescendantResizeHandler)m_scrollbar).onResizeDescendant();
406        }
407        maybeUpdateScrollbarPositions();
408    }
409
410    /**
411     * Set the scrollbar used for vertical scrolling.
412     *
413     * @param scrollbar the scrollbar, or null to clear it
414     * @param width the width of the scrollbar in pixels
415     */
416    private void setVerticalScrollbar(final CmsScrollBar scrollbar, int width) {
417
418        // Validate.
419        if ((scrollbar == m_scrollbar) || (scrollbar == null)) {
420            return;
421        }
422        // Detach new child.
423
424        scrollbar.asWidget().removeFromParent();
425        // Remove old child.
426        if (m_scrollbar != null) {
427            if (m_verticalScrollbarHandlerRegistration != null) {
428                m_verticalScrollbarHandlerRegistration.removeHandler();
429                m_verticalScrollbarHandlerRegistration = null;
430            }
431            remove(m_scrollbar);
432        }
433        m_scrollLayer.appendChild(scrollbar.asWidget().getElement());
434        adopt(scrollbar.asWidget());
435
436        // Logical attach.
437        m_scrollbar = scrollbar;
438        m_verticalScrollbarWidth = width;
439
440        // Initialize the new scrollbar.
441        m_verticalScrollbarHandlerRegistration = scrollbar.addValueChangeHandler(new ValueChangeHandler<Integer>() {
442
443            public void onValueChange(ValueChangeEvent<Integer> event) {
444
445                int vPos = scrollbar.getVerticalScrollPosition();
446                int v = getVerticalScrollPosition();
447                if (v != vPos) {
448                    setVerticalScrollPosition(vPos);
449                }
450
451            }
452        });
453        maybeUpdateScrollbars();
454    }
455}