#include "Base.h" #include "Container.h" #include "Layout.h" #include "AbsoluteLayout.h" #include "FlowLayout.h" #include "VerticalLayout.h" #include "Label.h" #include "Button.h" #include "CheckBox.h" #include "RadioButton.h" #include "Slider.h" #include "TextBox.h" #include "Joystick.h" #include "ImageControl.h" #include "Game.h" namespace gameplay { // If the user stops scrolling for this amount of time (in millis) before touch/click release, don't apply inertia. static const long SCROLL_INERTIA_DELAY = 100L; // Factor to multiply friction by before applying to velocity. static const float SCROLL_FRICTION_FACTOR = 5.0f; // Distance that must be scrolled before isScrolling() will return true, used e.g. to cancel button-click events. static const float SCROLL_THRESHOLD = 10.0f; // Distance a joystick must be pushed in order to trigger focus-change and/or scrolling. static const float JOYSTICK_THRESHOLD = 0.75f; // Scroll speed when using a DPad -- max scroll speed when using a joystick. static const float GAMEPAD_SCROLL_SPEED = 500.0f; // If the DPad or joystick is held down, this is the initial delay in milliseconds between focus change events. static const float FOCUS_CHANGE_REPEAT_DELAY = 300.0f; /** * Sort function for use with _controls.sort(), based on Z-Order. * * @param c1 The first control * @param c2 The second control * return true if the first controls z index is less than the second. */ static bool sortControlsByZOrder(Control* c1, Control* c2); Container::Container() : _layout(NULL), _scrollBarTopCap(NULL), _scrollBarVertical(NULL), _scrollBarBottomCap(NULL), _scrollBarLeftCap(NULL), _scrollBarHorizontal(NULL), _scrollBarRightCap(NULL), _scroll(SCROLL_NONE), _scrollBarBounds(Rectangle::empty()), _scrollPosition(Vector2::zero()), _scrollBarsAutoHide(false), _scrollBarOpacity(1.0f), _scrolling(false), _scrollingVeryFirstX(0), _scrollingVeryFirstY(0), _scrollingFirstX(0), _scrollingFirstY(0), _scrollingLastX(0), _scrollingLastY(0), _scrollingStartTimeX(0), _scrollingStartTimeY(0), _scrollingLastTime(0), _scrollingVelocity(Vector2::zero()), _scrollingFriction(1.0f), _scrollingRight(false), _scrollingDown(false), _scrollingMouseVertically(false), _scrollingMouseHorizontally(false), _scrollBarOpacityClip(NULL), _zIndexDefault(0), _focusIndexDefault(0), _focusIndexMax(0), _focusPressed(0), _selectButtonDown(false), _lastFrameTime(0), _focusChangeStartTime(0), _focusChangeRepeatDelay(FOCUS_CHANGE_REPEAT_DELAY), _focusChangeCount(0), _totalWidth(0), _totalHeight(0), _contactIndices(0), _initializedWithScroll(false) { } Container::~Container() { std::vector::iterator it; for (it = _controls.begin(); it < _controls.end(); it++) { SAFE_RELEASE((*it)); } SAFE_RELEASE(_layout); } Container* Container::create(const char* id, Theme::Style* style, Layout::Type layoutType) { GP_ASSERT(style); Container* container = Container::create(layoutType); if (id) container->_id = id; container->_style = style; return container; } Container* Container::create(Layout::Type type) { Layout* layout = NULL; switch (type) { case Layout::LAYOUT_ABSOLUTE: layout = AbsoluteLayout::create(); break; case Layout::LAYOUT_FLOW: layout = FlowLayout::create(); break; case Layout::LAYOUT_VERTICAL: layout = VerticalLayout::create(); break; } Container* container = new Container(); container->_layout = layout; return container; } Container* Container::create(Theme::Style* style, Properties* properties, Theme* theme) { GP_ASSERT(properties); const char* layoutString = properties->getString("layout"); Container* container = Container::create(getLayoutType(layoutString)); container->initialize(style, properties); container->_scroll = getScroll(properties->getString("scroll")); container->_scrollBarsAutoHide = properties->getBool("scrollBarsAutoHide"); if (container->_scrollBarsAutoHide) { container->_scrollBarOpacity = 0.0f; } container->addControls(theme, properties); container->_layout->update(container, container->_scrollPosition); return container; } void Container::addControls(Theme* theme, Properties* properties) { GP_ASSERT(theme); GP_ASSERT(properties); // Add all the controls to this container. Properties* controlSpace = properties->getNextNamespace(); while (controlSpace != NULL) { Control* control = NULL; const char* controlStyleName = controlSpace->getString("style"); Theme::Style* controlStyle = NULL; if (controlStyleName) { controlStyle = theme->getStyle(controlStyleName); } else { controlStyle = theme->getEmptyStyle(); } std::string controlName(controlSpace->getNamespace()); std::transform(controlName.begin(), controlName.end(), controlName.begin(), (int(*)(int))toupper); if (controlName == "LABEL") { control = Label::create(controlStyle, controlSpace); } else if (controlName == "BUTTON") { control = Button::create(controlStyle, controlSpace); } else if (controlName == "CHECKBOX") { control = CheckBox::create(controlStyle, controlSpace); } else if (controlName == "RADIOBUTTON") { control = RadioButton::create(controlStyle, controlSpace); } else if (controlName == "CONTAINER") { control = Container::create(controlStyle, controlSpace, theme); } else if (controlName == "SLIDER") { control = Slider::create(controlStyle, controlSpace); } else if (controlName == "TEXTBOX") { control = TextBox::create(controlStyle, controlSpace); } else if (controlName == "JOYSTICK") { control = Joystick::create(controlStyle, controlSpace); } else if (controlName == "IMAGE") { control = ImageControl::create(controlStyle, controlSpace); } else { // Ignore - not a valid control name. // This used to fail, but I see no reason to hard fail here (this also fixes not being able // to set padding on containers). } // Add the new control to the form. if (control) { addControl(control); control->release(); } // Get the next control. controlSpace = properties->getNextNamespace(); } // Sort controls by Z-Order. sortControls(); } Layout* Container::getLayout() { return _layout; } unsigned int Container::addControl(Control* control) { GP_ASSERT(control); if (control->_parent && control->_parent != this) { control->_parent->removeControl(control); } if (control->getZIndex() == -1) { control->setZIndex(_zIndexDefault++); } if (control->getFocusIndex() == -1) { control->setFocusIndex(_focusIndexDefault++); } int focusIndex = control->getFocusIndex(); if (focusIndex > _focusIndexMax) _focusIndexMax = focusIndex; if (control->_parent != this) { _controls.push_back(control); control->addRef(); control->_parent = this; sortControls(); return (unsigned int)(_controls.size() - 1); } else { // Control is already in this container. // Do nothing but determine and return control's index. const size_t size = _controls.size(); for (size_t i = 0; i < size; ++i) { Control* c = _controls[i]; if (c == control) { return (unsigned int)i; } } // Should never reach this. GP_ASSERT(false); return 0; } } void Container::insertControl(Control* control, unsigned int index) { GP_ASSERT(control); if (control->_parent && control->_parent != this) { control->_parent->removeControl(control); } if (control->_parent != this) { std::vector::iterator it = _controls.begin() + index; _controls.insert(it, control); control->addRef(); control->_parent = this; } } void Container::removeControl(unsigned int index) { GP_ASSERT(index < _controls.size()); std::vector::iterator it = _controls.begin() + index; _controls.erase(it); Control* control = *it; control->_parent = NULL; SAFE_RELEASE(control); } void Container::removeControl(const char* id) { GP_ASSERT(id); std::vector::iterator it; for (it = _controls.begin(); it < _controls.end(); it++) { Control* c = *it; if (strcmp(id, c->getId()) == 0) { c->_parent = NULL; SAFE_RELEASE(c); _controls.erase(it); return; } } } void Container::removeControl(Control* control) { GP_ASSERT(control); std::vector::iterator it; for (it = _controls.begin(); it < _controls.end(); it++) { if (*it == control) { control->_parent = NULL; SAFE_RELEASE(control); _controls.erase(it); return; } } } Control* Container::getControl(unsigned int index) const { std::vector::const_iterator it = _controls.begin() + index; return *it; } Control* Container::getControl(const char* id) const { GP_ASSERT(id); std::vector::const_iterator it; for (it = _controls.begin(); it < _controls.end(); it++) { Control* c = *it; GP_ASSERT(c); if (strcmp(id, c->getId()) == 0) { return c; } else if (c->isContainer()) { Control* cc = ((Container*)c)->getControl(id); if (cc) { return cc; } } } return NULL; } const std::vector& Container::getControls() const { return _controls; } void Container::setScroll(Scroll scroll) { if (scroll != _scroll) { _scroll = scroll; _dirty = true; } } Container::Scroll Container::getScroll() const { return _scroll; } void Container::setScrollBarsAutoHide(bool autoHide) { if (autoHide != _scrollBarsAutoHide) { _scrollBarsAutoHide = autoHide; _dirty = true; } } bool Container::isScrollBarsAutoHide() const { return _scrollBarsAutoHide; } bool Container::isScrolling() const { if (_parent && _parent->isScrolling()) return true; return (_scrolling && (abs(_scrollingLastX - _scrollingVeryFirstX) > SCROLL_THRESHOLD || abs(_scrollingLastY - _scrollingVeryFirstY) > SCROLL_THRESHOLD)); } Animation* Container::getAnimation(const char* id) const { std::vector::const_iterator itr = _controls.begin(); std::vector::const_iterator end = _controls.end(); Control* control = NULL; for (; itr != end; itr++) { control = *itr; GP_ASSERT(control); Animation* animation = control->getAnimation(id); if (animation) return animation; if (control->isContainer()) { animation = ((Container*)control)->getAnimation(id); if (animation) return animation; } } return NULL; } const char* Container::getType() const { return "container"; } void Container::update(const Control* container, const Vector2& offset) { // Update this container's viewport. Control::update(container, offset); // Get scrollbar images and diminish clipping bounds to make room for scrollbars. if ((_scroll & SCROLL_HORIZONTAL) == SCROLL_HORIZONTAL) { _scrollBarLeftCap = getImage("scrollBarLeftCap", _state); _scrollBarHorizontal = getImage("horizontalScrollBar", _state); _scrollBarRightCap = getImage("scrollBarRightCap", _state); GP_ASSERT(_scrollBarLeftCap && _scrollBarHorizontal && _scrollBarRightCap); _viewportClipBounds.height -= _scrollBarHorizontal->getRegion().height; } if ((_scroll & SCROLL_VERTICAL) == SCROLL_VERTICAL) { _scrollBarTopCap = getImage("scrollBarTopCap", _state); _scrollBarVertical = getImage("verticalScrollBar", _state); _scrollBarBottomCap = getImage("scrollBarBottomCap", _state); GP_ASSERT(_scrollBarTopCap && _scrollBarVertical && _scrollBarBottomCap); _viewportClipBounds.width -= _scrollBarVertical->getRegion().width; } GP_ASSERT(_layout); if (_scroll != SCROLL_NONE) { updateScroll(); } else { _layout->update(this, Vector2::zero()); } } void Container::draw(SpriteBatch* spriteBatch, const Rectangle& clip, bool needsClear, bool cleared, float targetHeight) { if (needsClear) { GL_ASSERT( glEnable(GL_SCISSOR_TEST) ); float clearY = targetHeight - _clearBounds.y - _clearBounds.height; GL_ASSERT( glScissor(_clearBounds.x, clearY, _clearBounds.width, _clearBounds.height) ); Game::getInstance()->clear(Game::CLEAR_COLOR, Vector4::zero(), 1.0f, 0); GL_ASSERT( glDisable(GL_SCISSOR_TEST) ); needsClear = false; cleared = true; } else if (!cleared) { needsClear = true; } if (!_visible) return; spriteBatch->start(); Control::drawBorder(spriteBatch, clip); spriteBatch->finish(); std::vector::const_iterator it; Rectangle boundsUnion = Rectangle::empty(); for (it = _controls.begin(); it < _controls.end(); it++) { Control* control = *it; GP_ASSERT(control); if (!needsClear || control->isDirty() || control->_clearBounds.intersects(boundsUnion)) { control->draw(spriteBatch, _viewportClipBounds, needsClear, cleared, targetHeight); Rectangle::combine(control->_clearBounds, boundsUnion, &boundsUnion); } } if (_scroll != SCROLL_NONE && (_scrollBarOpacity > 0.0f)) { // Draw scroll bars. Rectangle clipRegion(_viewportClipBounds); spriteBatch->start(); if (_scrollBarBounds.height > 0 &&((_scroll & SCROLL_VERTICAL) == SCROLL_VERTICAL)) { const Rectangle& topRegion = _scrollBarTopCap->getRegion(); const Theme::UVs& topUVs = _scrollBarTopCap->getUVs(); Vector4 topColor = _scrollBarTopCap->getColor(); topColor.w *= _scrollBarOpacity * _opacity; const Rectangle& verticalRegion = _scrollBarVertical->getRegion(); const Theme::UVs& verticalUVs = _scrollBarVertical->getUVs(); Vector4 verticalColor = _scrollBarVertical->getColor(); verticalColor.w *= _scrollBarOpacity * _opacity; const Rectangle& bottomRegion = _scrollBarBottomCap->getRegion(); const Theme::UVs& bottomUVs = _scrollBarBottomCap->getUVs(); Vector4 bottomColor = _scrollBarBottomCap->getColor(); bottomColor.w *= _scrollBarOpacity * _opacity; clipRegion.width += verticalRegion.width; Rectangle bounds(_viewportBounds.x + _viewportBounds.width - verticalRegion.width, _viewportBounds.y + _scrollBarBounds.y, topRegion.width, topRegion.height); spriteBatch->draw(bounds.x, bounds.y, bounds.width, bounds.height, topUVs.u1, topUVs.v1, topUVs.u2, topUVs.v2, topColor, clipRegion); bounds.y += topRegion.height; bounds.height = _scrollBarBounds.height - topRegion.height - bottomRegion.height; spriteBatch->draw(bounds.x, bounds.y, bounds.width, bounds.height, verticalUVs.u1, verticalUVs.v1, verticalUVs.u2, verticalUVs.v2, verticalColor, clipRegion); bounds.y += bounds.height; bounds.height = bottomRegion.height; spriteBatch->draw(bounds.x, bounds.y, bounds.width, bounds.height, bottomUVs.u1, bottomUVs.v1, bottomUVs.u2, bottomUVs.v2, bottomColor, clipRegion); } if (_scrollBarBounds.width > 0 && ((_scroll & SCROLL_HORIZONTAL) == SCROLL_HORIZONTAL)) { const Rectangle& leftRegion = _scrollBarLeftCap->getRegion(); const Theme::UVs& leftUVs = _scrollBarLeftCap->getUVs(); Vector4 leftColor = _scrollBarLeftCap->getColor(); leftColor.w *= _scrollBarOpacity * _opacity; const Rectangle& horizontalRegion = _scrollBarHorizontal->getRegion(); const Theme::UVs& horizontalUVs = _scrollBarHorizontal->getUVs(); Vector4 horizontalColor = _scrollBarHorizontal->getColor(); horizontalColor.w *= _scrollBarOpacity * _opacity; const Rectangle& rightRegion = _scrollBarRightCap->getRegion(); const Theme::UVs& rightUVs = _scrollBarRightCap->getUVs(); Vector4 rightColor = _scrollBarRightCap->getColor(); rightColor.w *= _scrollBarOpacity * _opacity; clipRegion.height += horizontalRegion.height; Rectangle bounds(_viewportBounds.x + _scrollBarBounds.x, _viewportBounds.y + _viewportBounds.height - horizontalRegion.height, leftRegion.width, leftRegion.height); spriteBatch->draw(bounds.x, bounds.y, bounds.width, bounds.height, leftUVs.u1, leftUVs.v1, leftUVs.u2, leftUVs.v2, leftColor, clipRegion); bounds.x += leftRegion.width; bounds.width = _scrollBarBounds.width - leftRegion.width - rightRegion.width; spriteBatch->draw(bounds.x, bounds.y, bounds.width, bounds.height, horizontalUVs.u1, horizontalUVs.v1, horizontalUVs.u2, horizontalUVs.v2, horizontalColor, clipRegion); bounds.x += bounds.width; bounds.width = rightRegion.width; spriteBatch->draw(bounds.x, bounds.y, bounds.width, bounds.height, rightUVs.u1, rightUVs.v1, rightUVs.u2, rightUVs.v2, rightColor, clipRegion); } spriteBatch->finish(); if (_scrollingVelocity.isZero()) { _dirty = false; } } else { _dirty = false; } } bool Container::isDirty() { if (_dirty) { return true; } else { std::vector::const_iterator it; for (it = _controls.begin(); it < _controls.end(); it++) { GP_ASSERT(*it); if ((*it)->isDirty()) { return true; } } } return false; } bool Container::touchEvent(Touch::TouchEvent evt, int x, int y, unsigned int contactIndex) { return pointerEvent(false, evt, x, y, (int)contactIndex); } bool Container::mouseEvent(Mouse::MouseEvent evt, int x, int y, int wheelDelta) { return pointerEvent(true, evt, x, y, wheelDelta); } bool Container::keyEvent(Keyboard::KeyEvent evt, int key) { // This event may run untrusted code by notifying listeners of events. // If the user calls exit() or otherwise releases this container, we // need to keep it alive until the method returns. addRef(); std::vector::const_iterator it; for (it = _controls.begin(); it < _controls.end(); it++) { Control* control = *it; GP_ASSERT(control); if (!control->isEnabled() || !control->isVisible()) { continue; } if (control->getState() == Control::FOCUS && control->keyEvent(evt, key)) { release(); return true; } } // If we made it this far, no control handled the event. if (evt == Keyboard::KEY_CHAR && key == Keyboard::KEY_TAB) { moveFocus(NEXT); return _consumeInputEvents; } release(); return false; } bool Container::moveFocus(Direction direction, Control* outsideControl) { Control* start = outsideControl; if (!start) { std::vector::const_iterator it; for (it = _controls.begin(); it < _controls.end(); it++) { Control* control = *it; GP_ASSERT(control); if (control->getState() == Control::FOCUS) { start = control; break; } } } int focusIndex = 0; Control* next = NULL; if (start) { const Rectangle& startBounds = start->getAbsoluteBounds(); Vector2 vStart, vNext; float distance = FLT_MAX; start->setState(Control::NORMAL); switch(direction) { case UP: vStart.set(startBounds.x + startBounds.width * 0.5f, startBounds.y); break; case DOWN: vStart.set(startBounds.x + startBounds.width * 0.5f, startBounds.bottom()); break; case LEFT: vStart.set(startBounds.x, startBounds.y + startBounds.height * 0.5f); break; case RIGHT: vStart.set(startBounds.right(), startBounds.y + startBounds.height * 0.5f); break; } if (direction != NEXT) { std::vector::const_iterator itt; for (itt = _controls.begin(); itt < _controls.end(); itt++) { Control* nextControl = *itt; if (nextControl == start || nextControl->getFocusIndex() < 0 || !nextControl->isEnabled() || !nextControl->isVisible()) { // Control is not focusable. continue; } const Rectangle& nextBounds = nextControl->getAbsoluteBounds(); switch(direction) { case UP: vNext.set(nextBounds.x + nextBounds.width * 0.5f, nextBounds.bottom()); if (vNext.y > vStart.y) continue; break; case DOWN: vNext.set(nextBounds.x + nextBounds.width * 0.5f, nextBounds.y); if (vNext.y < vStart.y) continue; break; case LEFT: vNext.set(nextBounds.right(), nextBounds.y + nextBounds.height * 0.5f); if (vNext.x > vStart.x) continue; break; case RIGHT: vNext.set(nextBounds.x, nextBounds.y + nextBounds.height * 0.5f); if (vNext.x < vStart.x) continue; break; } float nextDistance = vStart.distance(vNext); if (abs(nextDistance) < distance) { distance = nextDistance; next = nextControl; } } } if (!next) { // Check for controls in the given direction in our parent container. if (!outsideControl && _parent && _parent->moveFocus(direction, start)) { setState(NORMAL); _focusPressed = 0; return true; } // No control is in the given direction. Move to the next control in the focus order. int focusDelta; switch(direction) { case UP: case LEFT: focusDelta = -1; break; case DOWN: case RIGHT: case NEXT: focusDelta = 1; break; } // Find the index to search for. if (outsideControl) { focusIndex = outsideControl->_parent->getFocusIndex() + focusDelta; } else { focusIndex = start->getFocusIndex() + focusDelta; } if (focusIndex > _focusIndexMax) { focusIndex = 0; } else if (focusIndex < 0) { focusIndex = _focusIndexMax; } } } if (!next) { std::vector::const_iterator itt; for (itt = _controls.begin(); itt < _controls.end(); itt++) { Control* nextControl = *itt; if (nextControl->getFocusIndex() == focusIndex) { next = nextControl; } } } // If we haven't found next by now, then there are no focusable controls in this container. if (next) { next->setState(Control::FOCUS); _dirty = true; if (next->isContainer()) { if (((Container*)next)->moveFocus(direction, start)) { _focusPressed = 0; return true; } } // If the next control is not fully visible, scroll the container so that it is. const Rectangle& bounds = next->getBounds(); if (bounds.x < _scrollPosition.x) { // Control is to the left of the scrolled viewport. _scrollPosition.x = -bounds.x; } else if (bounds.x + bounds.width > _scrollPosition.x + _viewportBounds.width) { // Control is off to the right. _scrollPosition.x = -(bounds.x + bounds.width - _viewportBounds.width); } if (bounds.y < _viewportBounds.y - _scrollPosition.y) { // Control is above the viewport. _scrollPosition.y = -bounds.y; } else if (bounds.y + bounds.height > _viewportBounds.height - _scrollPosition.y) { // Control is below the viewport. _scrollPosition.y = -(bounds.y + bounds.height - _viewportBounds.height); } _focusChangeStartTime = Game::getAbsoluteTime(); if (outsideControl && outsideControl->_parent) { _focusPressed = direction; _focusChangeCount = outsideControl->_parent->_focusChangeCount; _focusChangeRepeatDelay = outsideControl->_parent->_focusChangeRepeatDelay; } addRef(); Game::getInstance()->schedule(_focusChangeRepeatDelay, this); return true; } return false; } void Container::timeEvent(long timeDiff, void* cookie) { double time = Game::getAbsoluteTime(); if (_state == FOCUS && _focusPressed && abs(time - timeDiff - _focusChangeRepeatDelay - _focusChangeStartTime) < 50) { ++_focusChangeCount; if (_focusChangeCount == 5) { _focusChangeRepeatDelay *= 0.5; } moveFocus((Direction)_focusPressed); } else { _focusChangeCount = 0; _focusChangeRepeatDelay = FOCUS_CHANGE_REPEAT_DELAY; } release(); } void Container::startScrolling(float x, float y, bool resetTime) { _scrollingVelocity.set(-x, y); _scrolling = true; _scrollBarOpacity = 1.0f; _dirty = true; if (_scrollBarOpacityClip && _scrollBarOpacityClip->isPlaying()) { _scrollBarOpacityClip->stop(); _scrollBarOpacityClip = NULL; } if (resetTime) { _lastFrameTime = Game::getGameTime(); } } void Container::stopScrolling() { _scrollingVelocity.set(0, 0); _scrolling = false; _dirty = true; } bool Container::gamepadEvent(Gamepad::GamepadEvent evt, Gamepad* gamepad, unsigned int analogIndex) { addRef(); bool eventConsumed = false; // Pass the event to any control that is active or in focus. std::vector::const_iterator it; for (it = _controls.begin(); it < _controls.end(); it++) { Control* control = *it; GP_ASSERT(control); if (control->getState() == Control::FOCUS || control->getState() == Control::ACTIVE) { eventConsumed |= control->gamepadEvent(evt, gamepad, analogIndex); if (eventConsumed && !control->isContainer()) { _focusPressed = 0; } break; } } // First check if a selection button is down. if (!_selectButtonDown) { if (gamepad->isButtonDown(Gamepad::BUTTON_A) || gamepad->isButtonDown(Gamepad::BUTTON_X)) { _selectButtonDown = true; _focusPressed = 0; eventConsumed |= _consumeInputEvents; } } else { if (!gamepad->isButtonDown(Gamepad::BUTTON_A) && !gamepad->isButtonDown(Gamepad::BUTTON_X)) { _selectButtonDown = false; } } Vector2 joystick; gamepad->getJoystickValues(analogIndex, &joystick); // Don't allow focus changes or scrolling while a selection button is down. if (!_selectButtonDown && !eventConsumed) { switch (evt) { case Gamepad::BUTTON_EVENT: { // Shift focus forward or backward when the DPad is used. if (!(_focusPressed & DOWN) && gamepad->isButtonDown(Gamepad::BUTTON_DOWN)) { _focusPressed |= DOWN; eventConsumed |= _consumeInputEvents; if (moveFocus(DOWN)) break; else startScrolling(0, -GAMEPAD_SCROLL_SPEED); } if (!(_focusPressed & RIGHT) && gamepad->isButtonDown(Gamepad::BUTTON_RIGHT)) { _focusPressed |= RIGHT; eventConsumed |= _consumeInputEvents; if (moveFocus(RIGHT)) break; else startScrolling(GAMEPAD_SCROLL_SPEED, 0); } if (!(_focusPressed & UP) && gamepad->isButtonDown(Gamepad::BUTTON_UP)) { _focusPressed |= UP; eventConsumed |= _consumeInputEvents; if (moveFocus(UP)) break; else startScrolling(0, GAMEPAD_SCROLL_SPEED); } if (!(_focusPressed & LEFT) && gamepad->isButtonDown(Gamepad::BUTTON_LEFT)) { _focusPressed |= LEFT; eventConsumed |= _consumeInputEvents; if (moveFocus(LEFT)) break; else startScrolling(-GAMEPAD_SCROLL_SPEED, 0); } break; } case Gamepad::JOYSTICK_EVENT: { switch (analogIndex) { case 0: // The left analog stick can be used in the same way as the DPad. eventConsumed |= _consumeInputEvents; if (!(_focusPressed & RIGHT) && joystick.x > JOYSTICK_THRESHOLD) { _focusPressed |= RIGHT; if (moveFocus(RIGHT)) break; else startScrolling(GAMEPAD_SCROLL_SPEED * joystick.x, 0); } if (!(_focusPressed & DOWN) && joystick.y < -JOYSTICK_THRESHOLD) { _focusPressed |= DOWN; if (moveFocus(DOWN)) break; else startScrolling(0, GAMEPAD_SCROLL_SPEED * joystick.y); } if (!(_focusPressed & LEFT) && joystick.x < -JOYSTICK_THRESHOLD) { _focusPressed |= LEFT; if (moveFocus(LEFT)) break; else startScrolling(GAMEPAD_SCROLL_SPEED * joystick.x, 0); } if (!(_focusPressed & UP) && joystick.y > JOYSTICK_THRESHOLD) { _focusPressed |= UP; if (moveFocus(UP)) break; else startScrolling(0, GAMEPAD_SCROLL_SPEED * joystick.y); } break; case 1: // The right analog stick can be used to scroll. if (_scrolling) { if (joystick.isZero()) { stopScrolling(); } else { startScrolling(GAMEPAD_SCROLL_SPEED * joystick.x, GAMEPAD_SCROLL_SPEED * joystick.y, false); } } else { startScrolling(GAMEPAD_SCROLL_SPEED * joystick.x, GAMEPAD_SCROLL_SPEED * joystick.y); } release(); return _consumeInputEvents; } } } } if ((_focusPressed & DOWN) && !gamepad->isButtonDown(Gamepad::BUTTON_DOWN) && joystick.y > -JOYSTICK_THRESHOLD) { _focusPressed &= ~DOWN; eventConsumed |= _consumeInputEvents; } if ((_focusPressed & RIGHT) && !gamepad->isButtonDown(Gamepad::BUTTON_RIGHT) && joystick.x < JOYSTICK_THRESHOLD) { _focusPressed &= ~RIGHT; eventConsumed |= _consumeInputEvents; } if ((_focusPressed & UP) && !gamepad->isButtonDown(Gamepad::BUTTON_UP) && joystick.y < JOYSTICK_THRESHOLD) { _focusPressed &= ~UP; eventConsumed |= _consumeInputEvents; } if ((_focusPressed & LEFT) && !gamepad->isButtonDown(Gamepad::BUTTON_LEFT) && joystick.x > -JOYSTICK_THRESHOLD) { _focusPressed &= ~LEFT; eventConsumed |= _consumeInputEvents; } if (!_focusPressed && _scrolling) { stopScrolling(); } release(); return eventConsumed; } bool Container::isContainer() const { return true; } Layout::Type Container::getLayoutType(const char* layoutString) { if (!layoutString) { return Layout::LAYOUT_ABSOLUTE; } std::string layoutName(layoutString); std::transform(layoutName.begin(), layoutName.end(), layoutName.begin(), (int(*)(int))toupper); if (layoutName == "LAYOUT_ABSOLUTE") { return Layout::LAYOUT_ABSOLUTE; } else if (layoutName == "LAYOUT_VERTICAL") { return Layout::LAYOUT_VERTICAL; } else if (layoutName == "LAYOUT_FLOW") { return Layout::LAYOUT_FLOW; } else { // Default. return Layout::LAYOUT_ABSOLUTE; } } void Container::updateScroll() { if (!_initializedWithScroll) { _initializedWithScroll = true; _layout->update(this, _scrollPosition); } // Update time. if (!_lastFrameTime) { _lastFrameTime = Game::getGameTime(); } double frameTime = Game::getGameTime(); float elapsedTime = (float)(frameTime - _lastFrameTime); _lastFrameTime = frameTime; const Theme::Border& containerBorder = getBorder(_state); const Theme::Padding& containerPadding = getPadding(); // Calculate total width and height. std::vector controls = getControls(); for (size_t i = 0, controlsCount = controls.size(); i < controlsCount; i++) { Control* control = controls.at(i); const Rectangle& bounds = control->getBounds(); const Theme::Margin& margin = control->getMargin(); float newWidth = bounds.x + bounds.width; if (newWidth > _totalWidth) { _totalWidth = newWidth; } float newHeight = bounds.y + bounds.height; if (newHeight > _totalHeight) { _totalHeight = newHeight; } } float vWidth = getImageRegion("verticalScrollBar", _state).width; float hHeight = getImageRegion("horizontalScrollBar", _state).height; float clipWidth = _bounds.width - containerBorder.left - containerBorder.right - containerPadding.left - containerPadding.right - vWidth; float clipHeight = _bounds.height - containerBorder.top - containerBorder.bottom - containerPadding.top - containerPadding.bottom - hHeight; // Apply and dampen inertia. if (!_scrollingVelocity.isZero()) { // Calculate the time passed since last update. float elapsedSecs = (float)elapsedTime * 0.001f; _scrollPosition.x += _scrollingVelocity.x * elapsedSecs; _scrollPosition.y += _scrollingVelocity.y * elapsedSecs; if (!_scrolling) { float dampening = 1.0f - _scrollingFriction * SCROLL_FRICTION_FACTOR * elapsedSecs; _scrollingVelocity.x *= dampening; _scrollingVelocity.y *= dampening; if (fabs(_scrollingVelocity.x) < 100.0f) _scrollingVelocity.x = 0.0f; if (fabs(_scrollingVelocity.y) < 100.0f) _scrollingVelocity.y = 0.0f; } } // Stop scrolling when the far edge is reached. if (-_scrollPosition.x > _totalWidth - clipWidth) { _scrollPosition.x = -(_totalWidth - clipWidth); _scrollingVelocity.x = 0; } if (-_scrollPosition.y > _totalHeight - clipHeight) { _scrollPosition.y = -(_totalHeight - clipHeight); _scrollingVelocity.y = 0; } if (_scrollPosition.x > 0) { _scrollPosition.x = 0; _scrollingVelocity.x = 0; } if (_scrollPosition.y > 0) { _scrollPosition.y = 0; _scrollingVelocity.y = 0; } float scrollWidth = 0; if (clipWidth < _totalWidth) scrollWidth = (clipWidth / _totalWidth) * clipWidth; float scrollHeight = 0; if (clipHeight < _totalHeight) scrollHeight = (clipHeight / _totalHeight) * clipHeight; _scrollBarBounds.set(((-_scrollPosition.x) / _totalWidth) * clipWidth, ((-_scrollPosition.y) / _totalHeight) * clipHeight, scrollWidth, scrollHeight); // If scroll velocity is 0 and scrollbars are not always visible, trigger fade-out animation. if (!_scrolling && _scrollingVelocity.isZero() && _scrollBarsAutoHide && _scrollBarOpacity == 1.0f) { float to = 0; _scrollBarOpacity = 0.99f; if (!_scrollBarOpacityClip) { Animation* animation = createAnimationFromTo("scrollbar-fade-out", ANIMATE_SCROLLBAR_OPACITY, &_scrollBarOpacity, &to, Curve::QUADRATIC_IN_OUT, 500L); _scrollBarOpacityClip = animation->getClip(); } _scrollBarOpacityClip->play(); } // Position controls within scroll area. _layout->update(this, _scrollPosition); } void Container::sortControls() { if (_layout->getType() == Layout::LAYOUT_ABSOLUTE) { std::sort(_controls.begin(), _controls.end(), &sortControlsByZOrder); } } bool Container::touchEventScroll(Touch::TouchEvent evt, int x, int y, unsigned int contactIndex) { switch(evt) { case Touch::TOUCH_PRESS: if (_contactIndex == INVALID_CONTACT_INDEX) { _contactIndex = (int) contactIndex; _contactIndices++; _scrollingLastX = _scrollingFirstX = _scrollingVeryFirstX = x; _scrollingLastY = _scrollingFirstY = _scrollingVeryFirstY = y; _scrollingVelocity.set(0, 0); _scrolling = true; _scrollingStartTimeX = _scrollingStartTimeY = 0; if (_scrollBarOpacityClip && _scrollBarOpacityClip->isPlaying()) { _scrollBarOpacityClip->stop(); _scrollBarOpacityClip = NULL; } _scrollBarOpacity = 1.0f; _dirty = true; return _consumeInputEvents; } break; case Touch::TOUCH_MOVE: if (_scrolling && _contactIndex == (int) contactIndex) { double gameTime = Game::getAbsoluteTime(); // Calculate the latest movement delta for the next update to use. int vx = x - _scrollingLastX; int vy = y - _scrollingLastY; if (_scrollingMouseVertically) { float yRatio = _totalHeight / _absoluteBounds.height; vy *= yRatio; _scrollingVelocity.set(0, -vy); _scrollPosition.y -= vy; } else if (_scrollingMouseHorizontally) { float xRatio = _totalWidth / _absoluteBounds.width; vx *= xRatio; _scrollingVelocity.set(-vx, 0); _scrollPosition.x -= vx; } else { _scrollingVelocity.set(vx, vy); _scrollPosition.x += vx; _scrollPosition.y += vy; } _scrollingLastX = x; _scrollingLastY = y; // If the user changes direction, reset the start time and position. bool goingRight = (vx > 0); if (goingRight != _scrollingRight) { _scrollingFirstX = x; _scrollingRight = goingRight; _scrollingStartTimeX = gameTime; } bool goingDown = (vy > 0); if (goingDown != _scrollingDown) { _scrollingFirstY = y; _scrollingDown = goingDown; _scrollingStartTimeY = gameTime; } if (!_scrollingStartTimeX) _scrollingStartTimeX = gameTime; if (!_scrollingStartTimeY) _scrollingStartTimeY = gameTime; _scrollingLastTime = gameTime; _dirty = true; return _consumeInputEvents; } break; case Touch::TOUCH_RELEASE: if (_contactIndex == (int) contactIndex) { _contactIndex = INVALID_CONTACT_INDEX; _contactIndices--; _scrolling = false; double gameTime = Game::getAbsoluteTime(); float timeSinceLastMove = (float)(gameTime - _scrollingLastTime); if (timeSinceLastMove > SCROLL_INERTIA_DELAY) { _scrollingVelocity.set(0, 0); _scrollingMouseVertically = _scrollingMouseHorizontally = false; _dirty = true; return _consumeInputEvents; } int dx = _scrollingLastX - _scrollingFirstX; int dy = _scrollingLastY - _scrollingFirstY; float timeTakenX = (float)(gameTime - _scrollingStartTimeX); float elapsedSecsX = timeTakenX * 0.001f; float timeTakenY = (float)(gameTime - _scrollingStartTimeY); float elapsedSecsY = timeTakenY * 0.001f; float vx = dx; float vy = dy; if (elapsedSecsX > 0) vx = (float)dx / elapsedSecsX; if (elapsedSecsY > 0) vy = (float)dy / elapsedSecsY; if (_scrollingMouseVertically) { float yRatio = _totalHeight / _absoluteBounds.height; vy *= yRatio; _scrollingVelocity.set(0, -vy); } else if (_scrollingMouseHorizontally) { float xRatio = _totalWidth / _absoluteBounds.width; vx *= xRatio; _scrollingVelocity.set(-vx, 0); } else { _scrollingVelocity.set(vx, vy); } _scrollingMouseVertically = _scrollingMouseHorizontally = false; _dirty = true; return _consumeInputEvents; } break; } return false; } bool Container::mouseEventScroll(Mouse::MouseEvent evt, int x, int y, int wheelDelta) { switch(evt) { case Mouse::MOUSE_PRESS_LEFT_BUTTON: { if (_scrollBarVertical) { float vWidth = _scrollBarVertical->getRegion().width; Rectangle vBounds(_viewportBounds.x + _viewportBounds.width - vWidth, _scrollBarBounds.y, vWidth, _scrollBarBounds.height); if (x + _viewportBounds.x >= vBounds.x && x + _viewportBounds.x <= vBounds.x + vBounds.width) { // Then we're within the horizontal bounds of the vertical scrollbar. // We want to either jump up or down, or drag the scrollbar itself. if (y < vBounds.y) { _scrollPosition.y += _totalHeight / 5.0f; } else if (y > vBounds.y + vBounds.height) { _scrollPosition.y -= _totalHeight / 5.0f; } else { _scrollingMouseVertically = true; } } } if (_scrollBarHorizontal) { float hHeight = _scrollBarHorizontal->getRegion().height; Rectangle hBounds(_scrollBarBounds.x, _viewportBounds.y + _viewportBounds.height - hHeight, _scrollBarBounds.width, hHeight); if (y + _viewportBounds.y >= hBounds.y && y + _viewportBounds.y <= hBounds.y + hBounds.height) { // We're within the vertical bounds of the horizontal scrollbar. if (x < hBounds.x) _scrollPosition.x += _totalWidth / 5.0f; else if (x > hBounds.x + hBounds.width) _scrollPosition.x -= _totalWidth / 5.0f; else _scrollingMouseHorizontally = true; } } return touchEventScroll(Touch::TOUCH_PRESS, x, y, 0); } case Mouse::MOUSE_MOVE: return touchEventScroll(Touch::TOUCH_MOVE, x, y, 0); case Mouse::MOUSE_RELEASE_LEFT_BUTTON: return touchEventScroll(Touch::TOUCH_RELEASE, x, y, 0); case Mouse::MOUSE_WHEEL: _scrollPosition.y += (_totalHeight / 10.0f) * wheelDelta; if (_scrollBarOpacityClip && _scrollBarOpacityClip->isPlaying()) { _scrollBarOpacityClip->stop(); _scrollBarOpacityClip = NULL; } _scrollBarOpacity = 1.0f; _dirty = true; return _consumeInputEvents; } return false; } bool Container::pointerEvent(bool mouse, char evt, int x, int y, int data) { if (!isEnabled() || !isVisible()) { return false; } // This event may run untrusted code by notifying listeners of events. // If the user calls exit() or otherwise releases this container, we // need to keep it alive until the method returns. addRef(); bool eventConsumed = false; const Theme::Border& border = getBorder(_state); const Theme::Padding& padding = getPadding(); float xPos = border.left + padding.left; float yPos = border.top + padding.top; Vector2* offset = NULL; if (_scroll != SCROLL_NONE) { offset = &_scrollPosition; } std::vector::const_iterator it; for (it = _controls.begin(); it < _controls.end(); it++) { Control* control = *it; GP_ASSERT(control); if (!control->isEnabled() || !control->isVisible()) { continue; } const Rectangle& bounds = control->getBounds(); float boundsX = bounds.x; float boundsY = bounds.y; if (offset) { boundsX += offset->x; boundsY += offset->y; } Control::State currentState = control->getState(); if ((control->isContainer() && currentState == Control::FOCUS) || (currentState != Control::NORMAL) || ((evt == Touch::TOUCH_PRESS || evt == Mouse::MOUSE_PRESS_LEFT_BUTTON || evt == Mouse::MOUSE_PRESS_MIDDLE_BUTTON || evt == Mouse::MOUSE_PRESS_RIGHT_BUTTON) && x >= xPos + boundsX && x <= xPos + boundsX + bounds.width && y >= yPos + boundsY && y <= yPos + boundsY + bounds.height)) { // Pass on the event's clip relative to the control. if (mouse) eventConsumed |= control->mouseEvent((Mouse::MouseEvent)evt, x - xPos - boundsX, y - yPos - boundsY, data); else eventConsumed |= control->touchEvent((Touch::TouchEvent)evt, x - xPos - boundsX, y - yPos - boundsY, (unsigned int)data); } } if (!isEnabled() || !isVisible()) { release(); return (_consumeInputEvents | eventConsumed); } bool withinClipBounds = false; switch (evt) { case Touch::TOUCH_PRESS: if (x > _clipBounds.x && x <= _clipBounds.x + _clipBounds.width && y > _clipBounds.y && y <= _clipBounds.y + _clipBounds.height) { setState(Control::FOCUS); withinClipBounds = true; if (eventConsumed) _contactIndices++; } else if (_contactIndices == 0) { setState(Control::NORMAL); _contactIndex = INVALID_CONTACT_INDEX; release(); return false; } break; case Touch::TOUCH_RELEASE: if (eventConsumed) { if (_contactIndices > 0) _contactIndices--; } break; } if (!eventConsumed && _scroll != SCROLL_NONE && getState() == Control::FOCUS && (evt != Touch::TOUCH_PRESS || withinClipBounds)) { if ((mouse && mouseEventScroll((Mouse::MouseEvent)evt, x - xPos, y - yPos, data)) || (!mouse && touchEventScroll((Touch::TouchEvent)evt, x - xPos, y - yPos, (unsigned int)data))) { eventConsumed = true; } } release(); if (x > _clipBounds.x && x <= _clipBounds.x + _clipBounds.width && y > _clipBounds.y && y <= _clipBounds.y + _clipBounds.height) return (_consumeInputEvents | eventConsumed); else return eventConsumed; } Container::Scroll Container::getScroll(const char* scroll) { if (!scroll) return Container::SCROLL_NONE; if (strcmp(scroll, "SCROLL_NONE") == 0) { return Container::SCROLL_NONE; } else if (strcmp(scroll, "SCROLL_HORIZONTAL") == 0) { return Container::SCROLL_HORIZONTAL; } else if (strcmp(scroll, "SCROLL_VERTICAL") == 0) { return Container::SCROLL_VERTICAL; } else if (strcmp(scroll, "SCROLL_BOTH") == 0) { return Container::SCROLL_BOTH; } else { GP_ERROR("Failed to get corresponding scroll state for unsupported value '%s'.", scroll); } return Container::SCROLL_NONE; } static bool sortControlsByZOrder(Control* c1, Control* c2) { if (c1->getZIndex() < c2->getZIndex()) return true; return false; } unsigned int Container::getAnimationPropertyComponentCount(int propertyId) const { switch(propertyId) { case ANIMATE_SCROLLBAR_OPACITY: return 1; default: return Control::getAnimationPropertyComponentCount(propertyId); } } void Container::getAnimationPropertyValue(int propertyId, AnimationValue* value) { GP_ASSERT(value); switch(propertyId) { case ANIMATE_SCROLLBAR_OPACITY: value->setFloat(0, _scrollBarOpacity); break; default: Control::getAnimationPropertyValue(propertyId, value); break; } } void Container::setAnimationPropertyValue(int propertyId, AnimationValue* value, float blendWeight) { GP_ASSERT(value); switch(propertyId) { case ANIMATE_SCROLLBAR_OPACITY: _scrollBarOpacity = Curve::lerp(blendWeight, _opacity, value->getFloat(0)); _dirty = true; break; default: Control::setAnimationPropertyValue(propertyId, value, blendWeight); break; } } }