/** * @file llmultisldr.cpp * @brief LLMultiSlider base class * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2010, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #include "linden_common.h" #include "llmultislider.h" #include "llui.h" #include "llgl.h" #include "llwindow.h" #include "llfocusmgr.h" #include "llkeyboard.h" // for the MASK constants #include "llcontrol.h" #include "lluictrlfactory.h" #include "lluiimage.h" #include static LLDefaultChildRegistry::Register r("multi_slider_bar"); const F32 FLOAT_THRESHOLD = 0.00001f; S32 LLMultiSlider::mNameCounter = 0; LLMultiSlider::SliderParams::SliderParams() : name("name"), value("value", 0.f) { } LLMultiSlider::Params::Params() : max_sliders("max_sliders", 1), allow_overlap("allow_overlap", false), loop_overlap("loop_overlap", false), orientation("orientation"), overlap_threshold("overlap_threshold", 0), draw_track("draw_track", true), use_triangle("use_triangle", false), track_color("track_color"), thumb_disabled_color("thumb_disabled_color"), thumb_highlight_color("thumb_highlight_color"), thumb_outline_color("thumb_outline_color"), thumb_center_color("thumb_center_color"), thumb_center_selected_color("thumb_center_selected_color"), thumb_image("thumb_image"), triangle_color("triangle_color"), mouse_down_callback("mouse_down_callback"), mouse_up_callback("mouse_up_callback"), thumb_width("thumb_width"), sliders("slider") {} LLMultiSlider::LLMultiSlider(const LLMultiSlider::Params& p) : LLF32UICtrl(p), mMouseOffset( 0 ), mMaxNumSliders(p.max_sliders), mAllowOverlap(p.allow_overlap), mLoopOverlap(p.loop_overlap), mDrawTrack(p.draw_track), mUseTriangle(p.use_triangle), mTrackColor(p.track_color()), mThumbOutlineColor(p.thumb_outline_color()), mThumbCenterColor(p.thumb_center_color()), mThumbCenterSelectedColor(p.thumb_center_selected_color()), mDisabledThumbColor(p.thumb_disabled_color()), mTriangleColor(p.triangle_color()), mThumbWidth(p.thumb_width), mOrientation((p.orientation() == "vertical") ? VERTICAL : HORIZONTAL), mMouseDownSignal(NULL), mMouseUpSignal(NULL) { mValue = LLSD::emptyMap(); mCurSlider = LLStringUtil::null; if (mOrientation == HORIZONTAL) { mDragStartThumbRect = LLRect(0, getRect().getHeight(), p.thumb_width, 0); } else { mDragStartThumbRect = LLRect(0, p.thumb_width, getRect().getWidth(), 0); } if (p.mouse_down_callback.isProvided()) { setMouseDownCallback(initCommitCallback(p.mouse_down_callback)); } if (p.mouse_up_callback.isProvided()) { setMouseUpCallback(initCommitCallback(p.mouse_up_callback)); } if (p.overlap_threshold.isProvided() && p.overlap_threshold > mIncrement) { mOverlapThreshold = p.overlap_threshold - mIncrement; } else { mOverlapThreshold = 0; } for (LLInitParam::ParamIterator::const_iterator it = p.sliders.begin(); it != p.sliders.end(); ++it) { if (it->name.isProvided()) { addSlider(it->value, it->name); } else { addSlider(it->value); } } mRoundedSquareImgp = LLUI::getUIImage("Rounded_Square"); if (p.thumb_image.isProvided()) { mThumbImagep = LLUI::getUIImage(p.thumb_image()); } mThumbHighlightColor = p.thumb_highlight_color.isProvided() ? p.thumb_highlight_color() : static_cast(gFocusMgr.getFocusColor()); } LLMultiSlider::~LLMultiSlider() { delete mMouseDownSignal; delete mMouseUpSignal; } F32 LLMultiSlider::getNearestIncrement(F32 value) const { value = llclamp(value, mMinValue, mMaxValue); // Round to nearest increment (bias towards rounding down) value -= mMinValue; value += mIncrement / 2.0001f; value -= fmod(value, mIncrement); return mMinValue + value; } void LLMultiSlider::setSliderValue(const std::string& name, F32 value, bool from_event) { // exit if not there if(!mValue.has(name)) { return; } F32 newValue = getNearestIncrement(value); // now, make sure no overlap // if we want that if(!mAllowOverlap) { bool hit = false; // look at the current spot // and see if anything is there LLSD::map_iterator mIt = mValue.beginMap(); // increment is our distance between points, use to eliminate round error F32 threshold = mOverlapThreshold + (mIncrement / 4); // If loop overlap is enabled, check if we overlap with points 'after' max value (project to lower) F32 loop_up_check = (mLoopOverlap && (value + threshold) > mMaxValue) ? (value + threshold - mMaxValue + mMinValue) : mMinValue - 1.0f; // If loop overlap is enabled, check if we overlap with points 'before' min value (project to upper) F32 loop_down_check = (mLoopOverlap && (value - threshold) < mMinValue) ? (value - threshold - mMinValue + mMaxValue) : mMaxValue + 1.0f; for(;mIt != mValue.endMap(); mIt++) { F32 locationVal = (F32)mIt->second.asReal(); // Check nearby values F32 testVal = locationVal - newValue; if (testVal > -threshold && testVal < threshold && mIt->first != name) { hit = true; break; } if (mLoopOverlap) { // Check edge overlap values if (locationVal < loop_up_check) { hit = true; break; } if (locationVal > loop_down_check) { hit = true; break; } } } // if none found, stop if(hit) { return; } } // now set it in the map mValue[name] = newValue; // set the control if it's the current slider and not from an event if (!from_event && name == mCurSlider) { setControlValue(mValue); } F32 t = (newValue - mMinValue) / (mMaxValue - mMinValue); if (mOrientation == HORIZONTAL) { S32 left_edge = mThumbWidth/2; S32 right_edge = getRect().getWidth() - (mThumbWidth/2); S32 x = left_edge + S32( t * (right_edge - left_edge) ); mThumbRects[name].mLeft = x - (mThumbWidth / 2); mThumbRects[name].mRight = x + (mThumbWidth / 2); } else { S32 bottom_edge = mThumbWidth/2; S32 top_edge = getRect().getHeight() - (mThumbWidth/2); S32 x = bottom_edge + S32( t * (top_edge - bottom_edge) ); mThumbRects[name].mTop = x + (mThumbWidth / 2); mThumbRects[name].mBottom = x - (mThumbWidth / 2); } } void LLMultiSlider::setValue(const LLSD& value) { // only do if it's a map if(value.isMap()) { // add each value... the first in the map becomes the current LLSD::map_const_iterator mIt = value.beginMap(); mCurSlider = mIt->first; for(; mIt != value.endMap(); mIt++) { setSliderValue(mIt->first, (F32)mIt->second.asReal(), true); } } } F32 LLMultiSlider::getSliderValue(const std::string& name) const { if (mValue.has(name)) { return (F32)mValue[name].asReal(); } return 0; } void LLMultiSlider::setCurSlider(const std::string& name) { if(mValue.has(name)) { mCurSlider = name; } } F32 LLMultiSlider::getSliderValueFromPos(S32 xpos, S32 ypos) const { F32 t = 0; if (mOrientation == HORIZONTAL) { S32 left_edge = mThumbWidth / 2; S32 right_edge = getRect().getWidth() - (mThumbWidth / 2); xpos += mMouseOffset; xpos = llclamp(xpos, left_edge, right_edge); t = F32(xpos - left_edge) / (right_edge - left_edge); } else { S32 bottom_edge = mThumbWidth / 2; S32 top_edge = getRect().getHeight() - (mThumbWidth / 2); ypos += mMouseOffset; ypos = llclamp(ypos, bottom_edge, top_edge); t = F32(ypos - bottom_edge) / (top_edge - bottom_edge); } return((t * (mMaxValue - mMinValue)) + mMinValue); } LLRect LLMultiSlider::getSliderThumbRect(const std::string& name) const { auto it = mThumbRects.find(name); if (it != mThumbRects.end()) return (*it).second; return LLRect(); } void LLMultiSlider::setSliderThumbImage(const std::string &name) { if (!name.empty()) { mThumbImagep = LLUI::getUIImage(name); } else clearSliderThumbImage(); } void LLMultiSlider::clearSliderThumbImage() { mThumbImagep = NULL; } void LLMultiSlider::resetCurSlider() { mCurSlider = LLStringUtil::null; } const std::string& LLMultiSlider::addSlider() { return addSlider(mInitialValue); } const std::string& LLMultiSlider::addSlider(F32 val) { std::stringstream newName; F32 initVal = val; if(mValue.size() >= mMaxNumSliders) { return LLStringUtil::null; } // create a new name newName << "sldr" << mNameCounter; mNameCounter++; bool foundOne = findUnusedValue(initVal); if(!foundOne) { return LLStringUtil::null; } // add a new thumb rect if (mOrientation == HORIZONTAL) { mThumbRects[newName.str()] = LLRect(0, getRect().getHeight(), mThumbWidth, 0); } else { mThumbRects[newName.str()] = LLRect(0, mThumbWidth, getRect().getWidth(), 0); } // add the value and set the current slider to this one mValue.insert(newName.str(), initVal); mCurSlider = newName.str(); // move the slider setSliderValue(mCurSlider, initVal, true); return mCurSlider; } bool LLMultiSlider::addSlider(F32 val, const std::string& name) { F32 initVal = val; if(mValue.size() >= mMaxNumSliders) { return false; } bool foundOne = findUnusedValue(initVal); if(!foundOne) { return false; } // add a new thumb rect if (mOrientation == HORIZONTAL) { mThumbRects[name] = LLRect(0, getRect().getHeight(), mThumbWidth, 0); } else { mThumbRects[name] = LLRect(0, mThumbWidth, getRect().getWidth(), 0); } // add the value and set the current slider to this one mValue.insert(name, initVal); mCurSlider = name; // move the slider setSliderValue(mCurSlider, initVal, true); return true; } bool LLMultiSlider::findUnusedValue(F32& initVal) { bool firstTry = true; // find the first open slot starting with // the initial value while(true) { bool hit = false; // look at the current spot // and see if anything is there F32 threshold = mAllowOverlap ? FLOAT_THRESHOLD : mOverlapThreshold + (mIncrement / 4); LLSD::map_iterator mIt = mValue.beginMap(); for(;mIt != mValue.endMap(); mIt++) { F32 testVal = (F32)mIt->second.asReal() - initVal; if(testVal > -threshold && testVal < threshold) { hit = true; break; } } // if we found one if(!hit) { break; } // increment and wrap if need be initVal += mIncrement; if(initVal > mMaxValue) { initVal = mMinValue; } // stop if it's filled if(initVal == mInitialValue && !firstTry) { LL_WARNS() << "Whoa! Too many multi slider elements to add one to" << LL_ENDL; return false; } firstTry = false; continue; } return true; } void LLMultiSlider::deleteSlider(const std::string& name) { // can't delete last slider if(mValue.size() <= 0) { return; } // get rid of value from mValue and its thumb rect mValue.erase(name); mThumbRects.erase(name); // set to the last created if(mValue.size() > 0) { std::map::iterator mIt = mThumbRects.end(); mIt--; mCurSlider = mIt->first; } } void LLMultiSlider::clear() { while(mThumbRects.size() > 0 && mValue.size() > 0) { deleteCurSlider(); } if (mThumbRects.size() > 0 || mValue.size() > 0) { LL_WARNS() << "Failed to fully clear Multi slider" << LL_ENDL; } LLF32UICtrl::clear(); } bool LLMultiSlider::handleHover(S32 x, S32 y, MASK mask) { if( gFocusMgr.getMouseCapture() == this ) { setCurSliderValue(getSliderValueFromPos(x, y)); onCommit(); getWindow()->setCursor(UI_CURSOR_ARROW); LL_DEBUGS("UserInput") << "hover handled by " << getName() << " (active)" << LL_ENDL; } else { if (getEnabled()) { mHoverSlider.clear(); std::map::iterator mIt = mThumbRects.begin(); for (; mIt != mThumbRects.end(); mIt++) { if (mIt->second.pointInRect(x, y)) { mHoverSlider = mIt->first; break; } } } else { mHoverSlider.clear(); } getWindow()->setCursor(UI_CURSOR_ARROW); LL_DEBUGS("UserInput") << "hover handled by " << getName() << " (inactive)" << LL_ENDL; } return true; } bool LLMultiSlider::handleMouseUp(S32 x, S32 y, MASK mask) { bool handled = false; if( gFocusMgr.getMouseCapture() == this ) { gFocusMgr.setMouseCapture( NULL ); if (mMouseUpSignal) (*mMouseUpSignal)( this, LLSD() ); handled = true; make_ui_sound("UISndClickRelease"); } else { handled = true; } return handled; } bool LLMultiSlider::handleMouseDown(S32 x, S32 y, MASK mask) { // only do sticky-focus on non-chrome widgets if (!getIsChrome()) { setFocus(true); } if (mMouseDownSignal) (*mMouseDownSignal)( this, LLSD() ); if (MASK_CONTROL & mask) // if CTRL is modifying { setCurSliderValue(mInitialValue); onCommit(); } else { // scroll through thumbs to see if we have a new one selected and select that one std::map::iterator mIt = mThumbRects.begin(); for(; mIt != mThumbRects.end(); mIt++) { // check if inside. If so, set current slider and continue if(mIt->second.pointInRect(x,y)) { mCurSlider = mIt->first; break; } } if (!mCurSlider.empty()) { // Find the offset of the actual mouse location from the center of the thumb. if (mThumbRects[mCurSlider].pointInRect(x,y)) { if (mOrientation == HORIZONTAL) { mMouseOffset = (mThumbRects[mCurSlider].mLeft + mThumbWidth / 2) - x; } else { mMouseOffset = (mThumbRects[mCurSlider].mBottom + mThumbWidth / 2) - y; } } else { mMouseOffset = 0; } // Start dragging the thumb // No handler needed for focus lost since this class has no state that depends on it. gFocusMgr.setMouseCapture( this ); mDragStartThumbRect = mThumbRects[mCurSlider]; } } make_ui_sound("UISndClick"); return true; } bool LLMultiSlider::handleKeyHere(KEY key, MASK mask) { bool handled = false; switch(key) { case KEY_UP: case KEY_DOWN: // eat up and down keys to be consistent handled = true; break; case KEY_LEFT: setCurSliderValue(getCurSliderValue() - getIncrement()); onCommit(); handled = true; break; case KEY_RIGHT: setCurSliderValue(getCurSliderValue() + getIncrement()); onCommit(); handled = true; break; default: break; } return handled; } /*virtual*/ void LLMultiSlider::onMouseLeave(S32 x, S32 y, MASK mask) { mHoverSlider.clear(); LLF32UICtrl::onMouseLeave(x, y, mask); } void LLMultiSlider::draw() { static LLUICachedControl extra_triangle_height ("UIExtraTriangleHeight", 0); static LLUICachedControl extra_triangle_width ("UIExtraTriangleWidth", 0); LLColor4 curThumbColor; std::map::iterator mIt; std::map::iterator curSldrIt; std::map::iterator hoverSldrIt; // Draw background and thumb. // drawing solids requires texturing be disabled gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); LLRect rect(mDragStartThumbRect); F32 opacity = getEnabled() ? 1.f : 0.3f; // Track static LLUICachedControl multi_track_height_width ("UIMultiTrackHeight", 0); S32 height_offset = 0; S32 width_offset = 0; if (mOrientation == HORIZONTAL) { height_offset = (getRect().getHeight() - multi_track_height_width) / 2; } else { width_offset = (getRect().getWidth() - multi_track_height_width) / 2; } LLRect track_rect(width_offset, getRect().getHeight() - height_offset, getRect().getWidth() - width_offset, height_offset); if(mDrawTrack) { track_rect.stretch(-1); mRoundedSquareImgp->draw(track_rect, mTrackColor.get() % opacity); } // if we're supposed to use a drawn triangle // simple gl call for the triangle if(mUseTriangle) { for(mIt = mThumbRects.begin(); mIt != mThumbRects.end(); mIt++) { gl_triangle_2d( mIt->second.mLeft - extra_triangle_width, mIt->second.mTop + extra_triangle_height, mIt->second.mRight + extra_triangle_width, mIt->second.mTop + extra_triangle_height, mIt->second.mLeft + mIt->second.getWidth() / 2, mIt->second.mBottom - extra_triangle_height, mTriangleColor.get() % opacity, true); } } else if (!mRoundedSquareImgp && !mThumbImagep) { // draw all the thumbs curSldrIt = mThumbRects.end(); hoverSldrIt = mThumbRects.end(); for(mIt = mThumbRects.begin(); mIt != mThumbRects.end(); mIt++) { // choose the color curThumbColor = mThumbCenterColor.get(); if(mIt->first == mCurSlider) { curSldrIt = mIt; continue; } if (mIt->first == mHoverSlider && getEnabled() && gFocusMgr.getMouseCapture() != this) { // draw last, after current one hoverSldrIt = mIt; continue; } // the draw command gl_rect_2d(mIt->second, curThumbColor, true); } // now draw the current and hover sliders if(curSldrIt != mThumbRects.end()) { gl_rect_2d(curSldrIt->second, mThumbCenterSelectedColor.get(), true); } // and draw the drag start if (gFocusMgr.getMouseCapture() == this) { gl_rect_2d(mDragStartThumbRect, mThumbCenterColor.get() % opacity, false); } else if (hoverSldrIt != mThumbRects.end()) { gl_rect_2d(hoverSldrIt->second, mThumbCenterSelectedColor.get(), true); } } else { LLMouseHandler* capture = gFocusMgr.getMouseCapture(); if (capture == this) { // draw drag start (ghost) if (mThumbImagep) { mThumbImagep->draw(mDragStartThumbRect, mThumbCenterColor.get() % 0.3f); } else { mRoundedSquareImgp->drawSolid(mDragStartThumbRect, mThumbCenterColor.get() % 0.3f); } } // draw the highlight if (hasFocus()) { if (!mCurSlider.empty()) { if (mThumbImagep) { mThumbImagep->drawBorder(mThumbRects[mCurSlider], mThumbHighlightColor, gFocusMgr.getFocusFlashWidth()); } else { mRoundedSquareImgp->drawBorder(mThumbRects[mCurSlider], gFocusMgr.getFocusColor(), gFocusMgr.getFocusFlashWidth()); } } } if (!mHoverSlider.empty()) { if (mThumbImagep) { mThumbImagep->drawBorder(mThumbRects[mHoverSlider], mThumbHighlightColor, gFocusMgr.getFocusFlashWidth()); } else { mRoundedSquareImgp->drawBorder(mThumbRects[mHoverSlider], gFocusMgr.getFocusColor(), gFocusMgr.getFocusFlashWidth()); } } // draw the thumbs curSldrIt = mThumbRects.end(); hoverSldrIt = mThumbRects.end(); for(mIt = mThumbRects.begin(); mIt != mThumbRects.end(); mIt++) { // choose the color curThumbColor = mThumbCenterColor.get(); if(mIt->first == mCurSlider) { // don't draw now, draw last curSldrIt = mIt; continue; } if (mIt->first == mHoverSlider && getEnabled() && gFocusMgr.getMouseCapture() != this) { // don't draw now, draw last, after current one hoverSldrIt = mIt; continue; } // the draw command if (mThumbImagep) { if (getEnabled()) { mThumbImagep->draw(mIt->second); } else { mThumbImagep->draw(mIt->second, LLColor4::grey % 0.8f); } } else if (capture == this) { mRoundedSquareImgp->drawSolid(mIt->second, curThumbColor); } else { mRoundedSquareImgp->drawSolid(mIt->second, curThumbColor % opacity); } } // draw cur and hover slider last if(curSldrIt != mThumbRects.end()) { if (mThumbImagep) { if (getEnabled()) { mThumbImagep->draw(curSldrIt->second); } else { mThumbImagep->draw(curSldrIt->second, LLColor4::grey % 0.8f); } } else if (capture == this) { mRoundedSquareImgp->drawSolid(curSldrIt->second, mThumbCenterSelectedColor.get()); } else { mRoundedSquareImgp->drawSolid(curSldrIt->second, mThumbCenterSelectedColor.get() % opacity); } } if(hoverSldrIt != mThumbRects.end()) { if (mThumbImagep) { mThumbImagep->draw(hoverSldrIt->second); } else { mRoundedSquareImgp->drawSolid(hoverSldrIt->second, mThumbCenterSelectedColor.get()); } } } LLF32UICtrl::draw(); } boost::signals2::connection LLMultiSlider::setMouseDownCallback( const commit_signal_t::slot_type& cb ) { if (!mMouseDownSignal) mMouseDownSignal = new commit_signal_t(); return mMouseDownSignal->connect(cb); } boost::signals2::connection LLMultiSlider::setMouseUpCallback( const commit_signal_t::slot_type& cb ) { if (!mMouseUpSignal) mMouseUpSignal = new commit_signal_t(); return mMouseUpSignal->connect(cb); }