Code owners
Assign users and groups as approvers for specific file changes. Learn more.
llmultislider.cpp 20.91 KiB
/**
* @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 <sstream>
static LLDefaultChildRegistry::Register<LLMultiSlider> 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<SliderParams>::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<LLUIColor>(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<std::string, LLRect>::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<std::string, LLRect>::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<std::string, LLRect>::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<S32> extra_triangle_height ("UIExtraTriangleHeight", 0);
static LLUICachedControl<S32> extra_triangle_width ("UIExtraTriangleWidth", 0);
LLColor4 curThumbColor;
std::map<std::string, LLRect>::iterator mIt;
std::map<std::string, LLRect>::iterator curSldrIt;
std::map<std::string, LLRect>::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<S32> 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);
}