001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (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.acacia.client;
029
030import org.opencms.acacia.client.css.I_CmsLayoutBundle;
031import org.opencms.acacia.client.ui.CmsAttributeChoiceWidget;
032import org.opencms.acacia.client.ui.CmsAttributeValueView;
033import org.opencms.acacia.client.ui.CmsChoiceMenuEntryWidget;
034import org.opencms.acacia.client.ui.CmsChoiceSubmenu;
035import org.opencms.acacia.client.ui.CmsInlineEntityWidget;
036import org.opencms.gwt.client.CmsCoreProvider;
037import org.opencms.gwt.client.util.CmsDomUtil;
038
039
040import java.util.ArrayList;
041import java.util.List;
042
043import com.google.gwt.dom.client.Element;
044import com.google.gwt.dom.client.EventTarget;
045import com.google.gwt.dom.client.NativeEvent;
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.user.client.Event;
051import com.google.gwt.user.client.Event.NativePreviewEvent;
052import com.google.gwt.user.client.Event.NativePreviewHandler;
053import com.google.gwt.user.client.EventListener;
054import com.google.gwt.user.client.Timer;
055import com.google.gwt.user.client.rpc.AsyncCallback;
056import com.google.gwt.user.client.ui.Widget;
057
058/**
059 * Helper class for controlling visibility of button hover bars of attribute value views.<p>
060 */
061public final class CmsButtonBarHandler implements MouseOverHandler, MouseOutHandler {
062
063    /** CSS class used for identifying widgets which normally react to hover events in touch-only mode. */
064    public static final String HOVERABLE_MARKER = "oc-editor-hoverable-marker";
065
066    /** Global instance of the button bar handler. */
067    public static final CmsButtonBarHandler INSTANCE = new CmsButtonBarHandler();
068
069    /** The timeout for hiding the buttons. */
070    public static final int TIMEOUT = 900;
071
072    /** The visible button bar.*/
073    Widget m_buttonBar;
074
075    /** The timer for hiding the button bar. */
076    private Timer m_buttonBarTimer;
077
078    /** The visible choice menu. */
079    private CmsAttributeChoiceWidget m_choice;
080
081    /** The timer for hiding the choice menu. */
082    private Timer m_choiceTimer;
083
084    /** The currently active submenus. */
085    private List<CmsChoiceSubmenu> m_submenus = new ArrayList<CmsChoiceSubmenu>();
086
087    /** The widget service. */
088    private I_CmsWidgetService m_widgetService;
089
090    /**
091     * Constructor.<p>
092     */
093    private CmsButtonBarHandler() {
094
095        Event.addNativePreviewHandler(new NativePreviewHandler() {
096
097            public void onPreviewNativeEvent(NativePreviewEvent event) {
098
099                NativeEvent nativeEvent = event.getNativeEvent();
100                if ((event.getTypeInt() != Event.ONMOUSEDOWN) && (event.getTypeInt() != Event.ONCLICK)) {
101                    return;
102                }
103                if (nativeEvent == null) {
104                    return;
105                }
106
107                if (handleSimulatedHoverForTouchOnlyDevices(event)) {
108                    return;
109                }
110
111                if (m_buttonBar == null) {
112                    return;
113                }
114                EventTarget target = nativeEvent.getEventTarget();
115
116                if (Element.is(target)) {
117                    Element targetElement = Element.as(target);
118                    boolean clickedOnMenu = m_buttonBar.getElement().isOrHasChild(targetElement);
119                    if (!clickedOnMenu) {
120                        closeAll();
121
122                    }
123                }
124            }
125
126            private boolean handleSimulatedHoverForTouchOnlyDevices(NativePreviewEvent previewEvent) {
127
128                NativeEvent nativeEvent = previewEvent.getNativeEvent();
129                boolean isMouseDown = previewEvent.getTypeInt() == Event.ONMOUSEDOWN;
130
131                if (!CmsCoreProvider.isTouchOnly()) {
132                    return false;
133                }
134
135                EventTarget target = nativeEvent.getEventTarget();
136                if (Element.is(target)) {
137                    Element element = Element.as(target);
138                    Element widgetElement = CmsDomUtil.getAncestor(element, HOVERABLE_MARKER);
139                    if (widgetElement != null) {
140                        EventListener widget = com.google.gwt.user.client.DOM.getEventListener(widgetElement);
141                        if (useClickAsFakeHover(widget, isMouseDown)) {
142                            previewEvent.cancel();
143                        }
144                    }
145                }
146                return false;
147            }
148        });
149        m_choiceTimer = new Timer() {
150
151            @Override
152            public void run() {
153
154                closeAllChoices();
155            }
156        };
157        m_buttonBarTimer = new Timer() {
158
159            @Override
160            public void run() {
161
162                closeAll();
163            }
164        };
165    }
166
167    /**
168     * Closes all visible button bars and menus.<p>
169     */
170    public void closeAll() {
171
172        if (m_buttonBar != null) {
173            setButtonBarVisibility(m_buttonBar, false);
174            m_buttonBar = null;
175        }
176        closeAllChoices();
177    }
178
179    /**
180     * @see com.google.gwt.event.dom.client.MouseOutHandler#onMouseOut(com.google.gwt.event.dom.client.MouseOutEvent)
181     */
182    public void onMouseOut(MouseOutEvent event) {
183
184        if (CmsCoreProvider.isTouchOnly()) {
185            return;
186        }
187
188        Object source = event.getSource();
189        if ((source instanceof CmsAttributeChoiceWidget) || (source instanceof CmsChoiceMenuEntryWidget)) {
190            rescheduleChoiceTimer();
191        } else {
192            rescheduleButtonBarTimer();
193        }
194    }
195
196    /**
197     * @see com.google.gwt.event.dom.client.MouseOverHandler#onMouseOver(com.google.gwt.event.dom.client.MouseOverEvent)
198     */
199    public void onMouseOver(MouseOverEvent event) {
200
201        if (CmsCoreProvider.isTouchOnly()) {
202            return;
203        }
204        cancelButtonBarTimer();
205        Object source = event.getSource();
206        if (source instanceof CmsAttributeChoiceWidget) {
207            overAttributeChoice((CmsAttributeChoiceWidget)source);
208        } else if (source instanceof CmsChoiceMenuEntryWidget) {
209            overChoiceEntry((CmsChoiceMenuEntryWidget)source);
210        } else {
211            overButtonBar((Widget)source);
212        }
213    }
214
215    /**
216     * Adds a new submenu.<p>
217     *
218     * @param entryWidget the entry widget whose children should be added to the submenu
219     */
220    protected void addSubmenu(CmsChoiceMenuEntryWidget entryWidget) {
221
222        CmsChoiceMenuEntryBean menuEntry = entryWidget.getEntryBean();
223        AsyncCallback<CmsChoiceMenuEntryBean> selectHandler = entryWidget.getSelectHandler();
224        CmsAttributeChoiceWidget choiceWidget = entryWidget.getAttributeChoiceWidget();
225        CmsChoiceSubmenu submenu = new CmsChoiceSubmenu(menuEntry);
226        submenu.positionDeferred(entryWidget);
227        choiceWidget.getSubmenuPanel().add(submenu);
228        m_submenus.add(submenu);
229        for (CmsChoiceMenuEntryBean subEntry : menuEntry.getChildren()) {
230            submenu.addChoice(
231                new CmsChoiceMenuEntryWidget(
232                    m_widgetService.getAttributeLabel(subEntry.getPathComponent()),
233                    m_widgetService.getAttributeHelp(subEntry.getPathComponent()),
234                    subEntry,
235                    selectHandler,
236                    choiceWidget,
237                    submenu));
238        }
239    }
240
241    /**
242     * Removes unnecessary submenus when the user hovers over a given menu entry.<p>
243     *
244     * @param entryWidget the menu entry over which the user is hovering
245     */
246    protected void cleanUpSubmenus(CmsChoiceMenuEntryWidget entryWidget) {
247
248        CmsChoiceSubmenu submenu = entryWidget.getSubmenu();
249        // First remove all submenus which are deeper than the submenu in which the current entry is located
250        while (!m_submenus.isEmpty() && (getLastSubmenu() != submenu)) {
251            removeSubmenu(getLastSubmenu());
252        }
253        // if it is a root entry, switch the attribute choice widget
254        if (submenu == null) {
255            CmsAttributeChoiceWidget choiceWidget = entryWidget.getAttributeChoiceWidget();
256            if (choiceWidget != m_choice) {
257                closeAllChoices();
258                m_choice = choiceWidget;
259            }
260        }
261    }
262
263    /**
264     * Gets the last entry in the current list of active submenus.<p>
265     *
266     * @return the last submenu
267     */
268    protected CmsChoiceSubmenu getLastSubmenu() {
269
270        return m_submenus.get(m_submenus.size() - 1);
271    }
272
273    /**
274     * Removes a submenu and hides it.<p>
275     *
276     * @param submenu the submenu to remove
277     */
278    protected void removeSubmenu(CmsChoiceSubmenu submenu) {
279
280        submenu.removeFromParent();
281        m_submenus.remove(submenu);
282    }
283
284    /**
285     * Sets the widget service.<p>
286     *
287     * @param widgetService the widget service
288     */
289    protected void setWidgetService(I_CmsWidgetService widgetService) {
290
291        m_widgetService = widgetService;
292    }
293
294    /**
295     * In touch-only mode, this is used to process some clicks to trigger actions that would be normally triggered by hovering over the same
296     * GUI element.
297     *
298     * @param source the event source
299     * @param isMouseDown true if the event is a mousedown event
300     *
301     * @return true if the event should be cancelled
302     */
303    protected boolean useClickAsFakeHover(EventListener source, boolean isMouseDown) {
304
305        boolean cancel = false;
306        if (source instanceof CmsAttributeChoiceWidget) {
307            if (isMouseDown) {
308                return false;
309            }
310            cancel = source != m_choice;
311            overAttributeChoice((CmsAttributeChoiceWidget)source);
312        } else if (source instanceof CmsChoiceMenuEntryWidget) {
313            if (isMouseDown) {
314                return false;
315            }
316            overChoiceEntry((CmsChoiceMenuEntryWidget)source);
317        } else {
318            if (m_buttonBar != source) {
319                // We either have a real menu bar or just a single button.
320                // In the first case we have to show the menu bar and cancel the click event, in the second case we don't.
321                // We distinguish between the two cases with a special attribute.
322                String hasHoverStr = ((Widget)source).getElement().getAttribute(CmsAttributeValueView.ATTR_HAS_HOVER);
323                if (Boolean.parseBoolean(hasHoverStr)) {
324                    overButtonBar((Widget)source);
325                    cancel = true;
326                }
327            }
328        }
329        return cancel;
330    }
331
332    /**
333     * Closes all currently active submenus and the root menu.<p>
334     */
335    void closeAllChoices() {
336
337        if (m_choice != null) {
338            m_choice.hide();
339        }
340        m_choice = null;
341        for (CmsChoiceSubmenu submenu : new ArrayList<CmsChoiceSubmenu>(m_submenus)) {
342            removeSubmenu(submenu);
343        }
344    }
345
346    /**
347     * Cancels the timer.<p>
348     */
349    private void cancelButtonBarTimer() {
350
351        m_buttonBarTimer.cancel();
352    }
353
354    /**
355     * Cancels the timer.<p>
356     */
357    private void cancelChoiceTimer() {
358
359        m_choiceTimer.cancel();
360    }
361
362    /**
363     * Handles the mouse over event for a choice widget.<p>
364     *
365     * @param choice the event source
366     */
367    private void overAttributeChoice(CmsAttributeChoiceWidget choice) {
368
369        cancelChoiceTimer();
370        if (choice.getParent() != m_buttonBar) {
371            closeAll();
372            m_buttonBar = choice.getParent();
373            setButtonBarVisibility(m_buttonBar, true);
374        }
375        if (m_choice != choice) {
376            closeAllChoices();
377            m_choice = choice;
378            m_choice.show();
379        }
380    }
381
382    /**
383     * Handles the mouse over event for a button bar.<p>
384     *
385     * @param buttonBar the event source
386     */
387    private void overButtonBar(Widget buttonBar) {
388
389        if ((m_buttonBar == null) || (buttonBar.getElement() != m_buttonBar.getElement())) {
390            closeAll();
391            m_buttonBar = buttonBar;
392            setButtonBarVisibility(m_buttonBar, true);
393        }
394    }
395
396    /**
397     * Handles the mouse over event for a choice menu entry.<p>
398     *
399     * @param entryWidget the event source
400     */
401    private void overChoiceEntry(CmsChoiceMenuEntryWidget entryWidget) {
402
403        cancelChoiceTimer();
404        cleanUpSubmenus(entryWidget);
405        CmsChoiceMenuEntryBean entryBean = entryWidget.getEntryBean();
406        if (!entryBean.isLeaf()) {
407            addSubmenu(entryWidget);
408        }
409    }
410
411    /**
412     * Reschedules the timer that hides the currently visible button bar.<p>
413     */
414    private void rescheduleButtonBarTimer() {
415
416        m_buttonBarTimer.cancel();
417        m_buttonBarTimer.schedule(TIMEOUT);
418    }
419
420    /**
421     * Reschedules the timer that hides the currently visible choice menu.<p>
422     */
423    private void rescheduleChoiceTimer() {
424
425        m_choiceTimer.cancel();
426        m_choiceTimer.schedule(TIMEOUT);
427    }
428
429    /**
430     * Sets the button bar visibility.<p>
431     *
432     * @param buttonBar the button bar
433     * @param visible <code>true</code> to show the button bar
434     */
435    private void setButtonBarVisibility(Widget buttonBar, boolean visible) {
436
437        String hoverStyle = I_CmsLayoutBundle.INSTANCE.form().hoverButton();
438        if (visible) {
439            buttonBar.addStyleName(hoverStyle);
440        } else {
441            buttonBar.removeStyleName(hoverStyle);
442        }
443        if (buttonBar instanceof CmsInlineEntityWidget) {
444            ((CmsInlineEntityWidget)buttonBar).setContentHighlightingVisible(visible);
445        }
446        if (buttonBar.getParent() instanceof CmsInlineEntityWidget) {
447            ((CmsInlineEntityWidget)buttonBar.getParent()).setContentHighlightingVisible(visible);
448        }
449    }
450}