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}