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.CmsDebugLog; 033import org.opencms.gwt.client.util.CmsPositionBean; 034 035import com.google.gwt.dom.client.Element; 036import com.google.gwt.dom.client.Style.Display; 037import com.google.gwt.dom.client.Style.Unit; 038import com.google.gwt.event.dom.client.KeyCodes; 039import com.google.gwt.event.dom.client.ScrollEvent; 040import com.google.gwt.event.dom.client.ScrollHandler; 041import com.google.gwt.event.logical.shared.ValueChangeEvent; 042import com.google.gwt.event.logical.shared.ValueChangeHandler; 043import com.google.gwt.event.shared.HandlerRegistration; 044import com.google.gwt.user.client.DOM; 045import com.google.gwt.user.client.Event; 046import com.google.gwt.user.client.Timer; 047import com.google.gwt.user.client.Window; 048import com.google.gwt.user.client.ui.FocusPanel; 049import com.google.gwt.user.client.ui.HasValue; 050import com.google.gwt.user.client.ui.VerticalScrollbar; 051 052/** 053 * A custom scroll bar to be used with {@link org.opencms.gwt.client.ui.CmsScrollPanel}.<p> 054 */ 055public class CmsScrollBar extends FocusPanel 056implements I_CmsDescendantResizeHandler, HasValue<Integer>, VerticalScrollbar { 057 058 /** 059 * The timer used to continue to shift the knob as the user holds down one of 060 * the left/right arrow keys. Only IE auto-repeats, so we just keep catching 061 * the events. 062 */ 063 private class KeyTimer extends Timer { 064 065 /** A bit indicating that this is the first run. */ 066 private boolean m_firstRun = true; 067 068 /** The number of steps to shift with each press. */ 069 private int m_multiplier = 1; 070 071 /** The delay between shifts, which shortens as the user holds down the button. */ 072 private final int m_repeatDelay = 30; 073 074 /** A bit indicating whether we are shifting to a higher or lower value. */ 075 private boolean m_shiftUp; 076 077 /** 078 * Constructor.<p> 079 */ 080 public KeyTimer() { 081 082 // nothing to do 083 } 084 085 /** 086 * This method will be called when a timer fires. Override it to implement 087 * the timer's logic. 088 */ 089 @Override 090 public void run() { 091 092 int newPos = 0; 093 boolean stop = false; 094 // Slide the slider bar 095 if (m_shiftUp) { 096 newPos = getVerticalScrollPosition() - (m_multiplier * m_stepSize); 097 } else { 098 newPos = getVerticalScrollPosition() + (m_multiplier * m_stepSize); 099 } 100 101 // Check if we are paging while holding mouse down 102 // and make sure will not overshoot original mouse down position. 103 if (m_pagingMouse && m_shiftUp && (m_mouseDownPos > newPos)) { 104 stop = true; 105 setValue(Integer.valueOf(m_mouseDownPos)); 106 } else if (m_pagingMouse && !m_shiftUp && (m_mouseDownPos < newPos)) { 107 stop = true; 108 setValue(Integer.valueOf(m_mouseDownPos)); 109 } 110 if (!stop) { 111 if (m_firstRun) { 112 m_firstRun = false; 113 } 114 115 // Slide the slider bar 116 setValue(Integer.valueOf(newPos)); 117 118 // Repeat this timer until cancelled by keyup event 119 schedule(m_repeatDelay); 120 } 121 } 122 123 /** 124 * Schedules a timer to elapse in the future. 125 * 126 * @param delayMillis how long to wait before the timer elapses, in 127 * milliseconds 128 * @param shiftUp2 whether to shift up or not 129 * @param multiplier2 the number of steps to shift 130 */ 131 public void schedule(final int delayMillis, final boolean shiftUp2, final int multiplier2) { 132 133 m_firstRun = true; 134 m_shiftUp = shiftUp2; 135 m_multiplier = multiplier2; 136 super.schedule(delayMillis); 137 } 138 } 139 140 /** The initial delay. */ 141 private static final int INITIALDELAY = 400; 142 143 /** The scroll knob minimum height. */ 144 private static final int SCROLL_KNOB_MIN_HEIGHT = 10; 145 146 /** The scroll knob top and bottom offset. */ 147 private static final int SCROLL_KNOB_OFFSET = 2; 148 149 /** The size of the increments between knob positions. */ 150 protected int m_stepSize = 5; 151 152 /** The position of the first mouse down. Used for paging. */ 153 int m_mouseDownPos; 154 155 /** A flag for the when the mouse button is held down when away from the slider. */ 156 boolean m_pagingMouse; 157 158 /** The scroll content element. */ 159 private Element m_containerElement; 160 161 /** The current scroll position. */ 162 private int m_currentValue; 163 164 /** 165 * The timer used to continue to shift the knob if the user holds down a key. 166 */ 167 private final KeyTimer m_keyTimer = new KeyTimer(); 168 169 /** The scroll knob. */ 170 private Element m_knob; 171 172 /** The current scroll knob height. */ 173 private int m_knobHeight; 174 175 /** Last mouse Y position. */ 176 private int m_lastMouseY; 177 178 /** Mous sliding start value. */ 179 private int m_mouseSlidingStartValue; 180 181 /** Mouse sliding start Y position. */ 182 private int m_mouseSlidingStartY; 183 184 /** The page size value. */ 185 private int m_pageSize = 20; 186 187 /** The value knob position ratio. */ 188 private double m_positionValueRatio; 189 190 /** The scrollable element. */ 191 private Element m_scrollableElement; 192 193 /** A bit indicating whether or not we are currently sliding the slider bar due to keyboard events. */ 194 private boolean m_slidingKeyboard; 195 196 /** 197 * A bit indicating whether or not we are currently sliding the slider bar due 198 * to mouse events. 199 */ 200 private boolean m_slidingMouse; 201 202 /** 203 * Constructor.<p> 204 * 205 * @param scrollableElement the scrollable element 206 * @param containerElement the scroll content 207 */ 208 public CmsScrollBar(Element scrollableElement, Element containerElement) { 209 210 I_CmsLayoutBundle.INSTANCE.scrollBarCss().ensureInjected(); 211 setStyleName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().scrollBar()); 212 m_scrollableElement = scrollableElement; 213 m_containerElement = containerElement; 214 m_knob = DOM.createDiv(); 215 m_knob.addClassName(I_CmsLayoutBundle.INSTANCE.scrollBarCss().scrollKnob()); 216 getElement().appendChild(m_knob); 217 sinkEvents(Event.MOUSEEVENTS | Event.ONMOUSEWHEEL | Event.KEYEVENTS | Event.FOCUSEVENTS); 218 } 219 220 /** 221 * @see com.google.gwt.event.dom.client.HasScrollHandlers#addScrollHandler(com.google.gwt.event.dom.client.ScrollHandler) 222 */ 223 public HandlerRegistration addScrollHandler(ScrollHandler handler) { 224 225 // Sink the event on the scrollable element, not the root element. 226 Event.sinkEvents(getScrollableElement(), Event.ONSCROLL); 227 return addHandler(handler, ScrollEvent.getType()); 228 } 229 230 /** 231 * @see com.google.gwt.event.logical.shared.HasValueChangeHandlers#addValueChangeHandler(com.google.gwt.event.logical.shared.ValueChangeHandler) 232 */ 233 public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Integer> handler) { 234 235 return addHandler(handler, ValueChangeEvent.getType()); 236 } 237 238 /** 239 * @see com.google.gwt.user.client.ui.HasVerticalScrolling#getMaximumVerticalScrollPosition() 240 */ 241 public int getMaximumVerticalScrollPosition() { 242 243 return m_containerElement.getOffsetHeight() - getScrollableElement().getOffsetHeight(); 244 } 245 246 /** 247 * @see com.google.gwt.user.client.ui.HasVerticalScrolling#getMinimumVerticalScrollPosition() 248 */ 249 public int getMinimumVerticalScrollPosition() { 250 251 return 0; 252 } 253 254 /** 255 * @see com.google.gwt.user.client.ui.VerticalScrollbar#getScrollHeight() 256 */ 257 public int getScrollHeight() { 258 259 return m_containerElement.getOffsetHeight(); 260 } 261 262 /** 263 * @see com.google.gwt.user.client.ui.HasValue#getValue() 264 */ 265 public Integer getValue() { 266 267 return Integer.valueOf(m_currentValue); 268 } 269 270 /** 271 * @see com.google.gwt.user.client.ui.HasVerticalScrolling#getVerticalScrollPosition() 272 */ 273 public int getVerticalScrollPosition() { 274 275 return getValue().intValue(); 276 } 277 278 /** 279 * @param reziseable true if the panel is resizeable 280 * 281 */ 282 public void isResizeable(boolean reziseable) { 283 284 if (reziseable) { 285 getElement().getStyle().setMarginBottom(7, Unit.PX); 286 } else { 287 getElement().getStyle().setMarginBottom(0, Unit.PX); 288 } 289 } 290 291 /** 292 * Listen for events that will move the knob. 293 * 294 * @param event the event that occurred 295 */ 296 @Override 297 public final void onBrowserEvent(final Event event) { 298 299 super.onBrowserEvent(event); 300 switch (DOM.eventGetType(event)) { 301 // Unhighlight and cancel keyboard events 302 case Event.ONBLUR: 303 m_keyTimer.cancel(); 304 if (m_slidingMouse) { 305 stopMouseSliding(event); 306 307 } else if (m_slidingKeyboard) { 308 m_slidingKeyboard = false; 309 310 } 311 312 break; 313 314 // Mousewheel events 315 case Event.ONMOUSEWHEEL: 316 int velocityY = event.getMouseWheelVelocityY() * m_stepSize; 317 event.preventDefault(); 318 CmsDebugLog.getInstance().printLine("Whell velocity: " + velocityY); 319 if (velocityY > 0) { 320 shiftDown(velocityY); 321 } else { 322 shiftUp(-velocityY); 323 } 324 break; 325 326 // Shift left or right on key press 327 case Event.ONKEYDOWN: 328 if (!m_slidingKeyboard) { 329 int multiplier = 1; 330 if (event.getCtrlKey() || event.getMetaKey()) { 331 multiplier = m_stepSize; 332 } 333 334 switch (event.getKeyCode()) { 335 case KeyCodes.KEY_HOME: 336 event.preventDefault(); 337 setValue(Integer.valueOf(0)); 338 break; 339 case KeyCodes.KEY_END: 340 event.preventDefault(); 341 setValue(Integer.valueOf(getMaximumVerticalScrollPosition())); 342 break; 343 case KeyCodes.KEY_PAGEUP: 344 event.preventDefault(); 345 m_slidingKeyboard = true; 346 shiftUp(m_pageSize); 347 m_keyTimer.schedule(INITIALDELAY, true, m_pageSize); 348 break; 349 case KeyCodes.KEY_PAGEDOWN: 350 event.preventDefault(); 351 m_slidingKeyboard = true; 352 353 shiftDown(m_pageSize); 354 m_keyTimer.schedule(INITIALDELAY, false, m_pageSize); 355 break; 356 case KeyCodes.KEY_UP: 357 event.preventDefault(); 358 m_slidingKeyboard = true; 359 360 shiftUp(multiplier); 361 m_keyTimer.schedule(INITIALDELAY, true, multiplier); 362 break; 363 case KeyCodes.KEY_DOWN: 364 event.preventDefault(); 365 m_slidingKeyboard = true; 366 367 shiftDown(multiplier); 368 m_keyTimer.schedule(INITIALDELAY, false, multiplier); 369 break; 370 371 default: 372 } 373 } 374 break; 375 // Stop shifting on key up 376 case Event.ONKEYUP: 377 m_keyTimer.cancel(); 378 if (m_slidingKeyboard) { 379 m_slidingKeyboard = false; 380 } 381 break; 382 383 // Mouse Events 384 case Event.ONMOUSEDOWN: 385 if (sliderClicked(event)) { 386 startMouseSliding(event); 387 event.preventDefault(); 388 } 389 break; 390 case Event.ONMOUSEUP: 391 stopMouseSliding(event); 392 break; 393 case Event.ONMOUSEMOVE: 394 slideKnob(event); 395 break; 396 default: 397 } 398 399 } 400 401 /** 402 * @see org.opencms.gwt.client.I_CmsDescendantResizeHandler#onResizeDescendant() 403 */ 404 public void onResizeDescendant() { 405 406 redraw(); 407 } 408 409 /** 410 * @see com.google.gwt.user.client.ui.VerticalScrollbar#setScrollHeight(int) 411 */ 412 public void setScrollHeight(int height) { 413 414 redraw(); 415 } 416 417 /** 418 * @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object) 419 */ 420 public void setValue(Integer value) { 421 422 setValue(value, true); 423 } 424 425 /** 426 * @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object, boolean) 427 */ 428 public void setValue(Integer value, boolean fireEvents) { 429 430 if (value != null) { 431 m_currentValue = value.intValue(); 432 } else { 433 m_currentValue = 0; 434 } 435 setKnobPosition(m_currentValue); 436 // Fire the ValueChangeEvent 437 if (fireEvents) { 438 ValueChangeEvent.fire(this, Integer.valueOf(m_currentValue)); 439 } 440 } 441 442 /** 443 * @see com.google.gwt.user.client.ui.HasVerticalScrolling#setVerticalScrollPosition(int) 444 */ 445 public void setVerticalScrollPosition(int position) { 446 447 setValue(Integer.valueOf(position)); 448 } 449 450 /** 451 * Returns the associated scrollable element.<p> 452 * 453 * @return the associated scrollable element 454 */ 455 protected Element getScrollableElement() { 456 457 return m_scrollableElement; 458 } 459 460 /** 461 * @see com.google.gwt.user.client.ui.Widget#onAttach() 462 */ 463 @Override 464 protected void onAttach() { 465 466 super.onAttach(); 467 468 /* 469 * Attach the event listener in onAttach instead of onLoad so users cannot 470 * accidentally override it. 471 */ 472 Event.setEventListener(getScrollableElement(), this); 473 redraw(); 474 } 475 476 /** 477 * @see com.google.gwt.user.client.ui.Widget#onDetach() 478 */ 479 @Override 480 protected void onDetach() { 481 482 /* 483 * Detach the event listener in onDetach instead of onUnload so users cannot 484 * accidentally override it. 485 */ 486 Event.setEventListener(getScrollableElement(), null); 487 488 super.onDetach(); 489 } 490 491 /** 492 * Redraws the scroll bar.<p> 493 */ 494 protected void redraw() { 495 496 if (isAttached()) { 497 int outerHeight = getElement().getOffsetHeight(); 498 int innerHeight = m_containerElement.getOffsetHeight(); 499 if (outerHeight >= innerHeight) { 500 setScrollbarVisible(false); 501 } else { 502 setScrollbarVisible(true); 503 adjustKnobHeight(outerHeight, innerHeight); 504 setKnobPosition(m_currentValue); 505 } 506 } 507 if (m_slidingMouse) { 508 m_mouseSlidingStartY = m_lastMouseY; 509 m_mouseSlidingStartValue = m_currentValue; 510 } 511 } 512 513 /** 514 * Shifts the scroll position down.<p> 515 * 516 * @param shift the shift size 517 */ 518 protected void shiftDown(int shift) { 519 520 int max = getMaximumVerticalScrollPosition(); 521 if ((m_currentValue + shift) < max) { 522 setVerticalScrollPosition(m_currentValue + shift); 523 } else { 524 setVerticalScrollPosition(max); 525 } 526 } 527 528 /** 529 * Shifts the scroll position up.<p> 530 * 531 * @param shift the shift size 532 */ 533 protected void shiftUp(int shift) { 534 535 int min = getMinimumVerticalScrollPosition(); 536 if ((m_currentValue - shift) > min) { 537 setVerticalScrollPosition(m_currentValue - shift); 538 } else { 539 setVerticalScrollPosition(min); 540 } 541 } 542 543 /** 544 * Calculates the scroll knob height.<p> 545 * 546 * @param outerHeight the height of the scrollable element 547 * @param innerHeight the height of the scroll content 548 */ 549 private void adjustKnobHeight(int outerHeight, int innerHeight) { 550 551 int result = (int)((1.0 * outerHeight * outerHeight) / innerHeight); 552 result = result > (outerHeight - 5) ? 5 : (result < 8 ? 8 : result); 553 m_positionValueRatio = (1.0 * (outerHeight - result)) / (innerHeight - outerHeight); 554 m_knobHeight = result - (2 * SCROLL_KNOB_OFFSET); 555 m_knobHeight = m_knobHeight < SCROLL_KNOB_MIN_HEIGHT ? SCROLL_KNOB_MIN_HEIGHT : m_knobHeight; 556 m_knob.getStyle().setHeight(m_knobHeight, Unit.PX); 557 } 558 559 /** 560 * Sets the scroll knob position according to the given value.<p> 561 * 562 * @param value the value 563 */ 564 private void setKnobPosition(int value) { 565 566 int top = (int)(SCROLL_KNOB_OFFSET + (m_positionValueRatio * value)); 567 int maxPosition = getElement().getOffsetHeight() - m_knobHeight - SCROLL_KNOB_OFFSET; 568 top = top < SCROLL_KNOB_OFFSET ? SCROLL_KNOB_OFFSET : (top > maxPosition ? maxPosition : top); 569 m_knob.getStyle().setTop(top, Unit.PX); 570 } 571 572 /** 573 * Sets the scroll bar visibility.<p> 574 * 575 * @param visible <code>true</code> to set the scroll bar visible 576 */ 577 private void setScrollbarVisible(boolean visible) { 578 579 if (visible) { 580 getElement().getStyle().clearWidth(); 581 m_knob.getStyle().clearDisplay(); 582 } else { 583 getElement().getStyle().setWidth(0, Unit.PX); 584 m_knob.getStyle().setDisplay(Display.NONE); 585 } 586 } 587 588 /** 589 * Sides the scroll knob according to the mouse event.<p> 590 * 591 * @param event the mouse event 592 */ 593 private void slideKnob(Event event) { 594 595 if (m_slidingMouse) { 596 m_lastMouseY = event.getClientY(); 597 int shift = (int)((m_lastMouseY - m_mouseSlidingStartY) / m_positionValueRatio); 598 int nextValue = m_mouseSlidingStartValue + shift; 599 CmsDebugLog.getInstance().printLine("Mouse sliding should set value to: " + nextValue); 600 int max = getMaximumVerticalScrollPosition(); 601 int min = getMinimumVerticalScrollPosition(); 602 if (nextValue < min) { 603 nextValue = min; 604 } else if (nextValue > max) { 605 nextValue = max; 606 } 607 setValue(Integer.valueOf(nextValue)); 608 } 609 } 610 611 /** 612 * Returns <code>true</code> if the events mouse position is above the scroll bar knob.<p> 613 * 614 * @param event the mouse event 615 * 616 * @return <code>true</code> if the events mouse position is above the scroll bar knob 617 */ 618 private boolean sliderClicked(Event event) { 619 620 boolean result = CmsPositionBean.generatePositionInfo(m_knob).isOverElement( 621 event.getClientX() + Window.getScrollLeft(), 622 event.getClientY() + Window.getScrollTop()); 623 CmsDebugLog.getInstance().printLine("Slider was clicked: " + result); 624 return result; 625 } 626 627 /** 628 * Starts the mouse sliding.<p> 629 * 630 * @param event the mouse event 631 */ 632 private void startMouseSliding(Event event) { 633 634 if (!m_slidingMouse) { 635 m_slidingMouse = true; 636 DOM.setCapture(getElement()); 637 m_mouseSlidingStartY = event.getClientY(); 638 m_mouseSlidingStartValue = m_currentValue; 639 CmsDebugLog.getInstance().printLine( 640 "Mouse sliding started with clientY: " 641 + m_mouseSlidingStartY 642 + " and start value: " 643 + m_mouseSlidingStartValue 644 + " and a max value of " 645 + getMaximumVerticalScrollPosition()); 646 } 647 } 648 649 /** 650 * Stops the mouse sliding.<p> 651 * 652 * @param event the mouse event 653 */ 654 private void stopMouseSliding(Event event) { 655 656 slideKnob(event); 657 m_slidingMouse = false; 658 DOM.releaseCapture(getElement()); 659 } 660}