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}