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