Did you know you could type into a list to search it by prefix?! The Lindens didn't!! Let's make this search bar an ACTUAL FILTER! To do this, we add support for filtering to all scroll lists, so that's cool. Also filtered items will be selected. Also optimized item selection loop to perform better when selecting in massive lists (like group members) Changed the text on the Copy Button to reflect that it copies everything, not just the applied filter... if you wanted that, you could just ctrl-a, ctrl-c... I mean, reallly
3375 lines
79 KiB
C++
3375 lines
79 KiB
C++
/**
|
|
* @file llscrolllistctrl.cpp
|
|
* @brief Scroll lists are composed of rows (items), each of which
|
|
* contains columns (cells).
|
|
*
|
|
* $LicenseInfo:firstyear=2001&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 "llscrolllistctrl.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "llstl.h"
|
|
#include "llboost.h"
|
|
//#include "indra_constants.h"
|
|
|
|
#include "llcheckboxctrl.h"
|
|
#include "llclipboard.h"
|
|
#include "llfocusmgr.h"
|
|
#include "lllocalcliprect.h"
|
|
//#include "llrender.h"
|
|
#include "llresmgr.h"
|
|
#include "llscrollbar.h"
|
|
#include "llscrolllistcell.h"
|
|
#include "llscrolllistcolumn.h"
|
|
#include "llscrolllistitem.h"
|
|
#include "llstring.h"
|
|
#include "llui.h"
|
|
#include "lluictrlfactory.h"
|
|
#include "llwindow.h"
|
|
#include "llcontrol.h"
|
|
#include "llkeyboard.h"
|
|
#include "llsdparam.h"
|
|
#include "llmenugl.h"
|
|
|
|
#include <boost/algorithm/string/predicate.hpp>
|
|
|
|
|
|
static LLRegisterWidget<LLScrollListCtrl> r("scroll_list");
|
|
|
|
std::vector<LLMenuGL*> LLScrollListCtrl::sMenus = {}; // List menus that recur, such as general avatars or groups menus
|
|
|
|
// local structures & classes.
|
|
struct SortScrollListItem
|
|
{
|
|
SortScrollListItem(const std::vector<LLScrollListCtrl::sort_column_t >& sort_orders,const LLScrollListCtrl::sort_signal_t* sort_signal)
|
|
: mSortOrders(sort_orders)
|
|
, mSortSignal(sort_signal)
|
|
{}
|
|
|
|
bool operator()(const LLScrollListItem* i1, const LLScrollListItem* i2)
|
|
{
|
|
// sort over all columns in order specified by mSortOrders
|
|
S32 sort_result = 0;
|
|
for (sort_order_t::const_reverse_iterator it = mSortOrders.rbegin();
|
|
it != mSortOrders.rend(); ++it)
|
|
{
|
|
S32 col_idx = it->first;
|
|
BOOL sort_ascending = it->second;
|
|
|
|
S32 order = sort_ascending ? 1 : -1; // ascending or descending sort for this column?
|
|
|
|
const LLScrollListCell *cell1 = i1->getColumn(col_idx);
|
|
const LLScrollListCell *cell2 = i2->getColumn(col_idx);
|
|
if (cell1 && cell2)
|
|
{
|
|
if(mSortSignal)
|
|
{
|
|
sort_result = order * (*mSortSignal)(col_idx,i1, i2);
|
|
}
|
|
else
|
|
{
|
|
sort_result = order * LLStringUtil::compareDict(cell1->getValue().asString(), cell2->getValue().asString());
|
|
}
|
|
if (sort_result != 0)
|
|
{
|
|
break; // we have a sort order!
|
|
}
|
|
}
|
|
}
|
|
|
|
return sort_result < 0;
|
|
}
|
|
|
|
|
|
typedef std::vector<LLScrollListCtrl::sort_column_t > sort_order_t;
|
|
const LLScrollListCtrl::sort_signal_t* mSortSignal;
|
|
const sort_order_t& mSortOrders;
|
|
};
|
|
|
|
//---------------------------------------------------------------------------
|
|
// LLScrollListCtrl
|
|
//---------------------------------------------------------------------------
|
|
|
|
LLScrollListCtrl::LLScrollListCtrl(const std::string& name, const LLRect& rect, commit_callback_t commit_callback, bool multi_select, bool has_border, bool draw_heading)
|
|
: LLUICtrl(name, rect, TRUE, commit_callback),
|
|
mLineHeight(0),
|
|
mScrollLines(0),
|
|
mMouseWheelOpaque(true),
|
|
mPageLines(0),
|
|
mMaxSelectable(0),
|
|
mAllowKeyboardMovement(true),
|
|
mCommitOnKeyboardMovement(true),
|
|
mCommitOnSelectionChange(false),
|
|
mSelectionChanged(false),
|
|
mNeedsScroll(false),
|
|
mCanSelect(true),
|
|
mColumnsDirty(false),
|
|
mSortEnabled(true),
|
|
mMaxItemCount(INT_MAX),
|
|
mMaxContentWidth(0),
|
|
mBorderThickness( 2 ),
|
|
mOnDoubleClickCallback( NULL ),
|
|
mOnMaximumSelectCallback( NULL ),
|
|
mOnSortChangedCallback( NULL ),
|
|
mHighlightedItem(-1),
|
|
mBorder(NULL),
|
|
mSortCallback(NULL),
|
|
mPopupMenu(NULL),
|
|
mCommentTextView(NULL),
|
|
mNumDynamicWidthColumns(0),
|
|
mTotalStaticColumnWidth(0),
|
|
mTotalColumnPadding(0),
|
|
mSorted(true),
|
|
mDirty(false),
|
|
mOriginalSelection(-1),
|
|
mLastSelected(NULL),
|
|
mHeadingHeight(20),
|
|
mAllowMultipleSelection(multi_select),
|
|
mDisplayColumnHeaders(draw_heading),
|
|
mBackgroundVisible(true),
|
|
mDrawStripes(true),
|
|
mBgWriteableColor(LLUI::sColorsGroup->getColor("ScrollBgWriteableColor")),
|
|
mBgReadOnlyColor(LLUI::sColorsGroup->getColor("ScrollBgReadOnlyColor")),
|
|
mBgSelectedColor(LLUI::sColorsGroup->getColor("ScrollSelectedBGColor")),
|
|
mBgStripeColor(LLUI::sColorsGroup->getColor("ScrollBGStripeColor")),
|
|
mFgSelectedColor(LLUI::sColorsGroup->getColor("ScrollSelectedFGColor")),
|
|
mFgUnselectedColor(LLUI::sColorsGroup->getColor("ScrollUnselectedColor")),
|
|
mFgDisabledColor(LLUI::sColorsGroup->getColor("ScrollDisabledColor")),
|
|
mHighlightedColor(LLUI::sColorsGroup->getColor("ScrollHighlightedColor")),
|
|
mSearchColumn(0),
|
|
mColumnPadding(5)
|
|
{
|
|
mItemListRect.setOriginAndSize(
|
|
mBorderThickness,
|
|
mBorderThickness,
|
|
getRect().getWidth() - 2 * mBorderThickness,
|
|
getRect().getHeight() - 2 * mBorderThickness );
|
|
|
|
updateLineHeight();
|
|
|
|
// Init the scrollbar
|
|
LLRect scroll_rect;
|
|
scroll_rect.setOriginAndSize(
|
|
getRect().getWidth() - mBorderThickness - SCROLLBAR_SIZE,
|
|
mItemListRect.mBottom,
|
|
SCROLLBAR_SIZE,
|
|
mItemListRect.getHeight());
|
|
mScrollbar = new LLScrollbar( std::string("Scrollbar"), scroll_rect,
|
|
LLScrollbar::VERTICAL,
|
|
getItemCount(),
|
|
mScrollLines,
|
|
getLinesPerPage(),
|
|
boost::bind(&LLScrollListCtrl::onScrollChange, this, _1, _2) );
|
|
mScrollbar->setFollows(FOLLOWS_RIGHT | FOLLOWS_TOP | FOLLOWS_BOTTOM);
|
|
mScrollbar->setEnabled(true);
|
|
// scrollbar is visible only when needed
|
|
mScrollbar->setVisible(false);
|
|
addChild(mScrollbar);
|
|
|
|
// Border
|
|
if (has_border)
|
|
{
|
|
LLRect border_rect( 0, getRect().getHeight(), getRect().getWidth(), 0 );
|
|
mBorder = new LLViewBorder( std::string("dlg border"), border_rect, LLViewBorder::BEVEL_IN, LLViewBorder::STYLE_LINE, 1 );
|
|
addChild(mBorder);
|
|
}
|
|
|
|
LLTextBox* textBox = new LLTextBox("comment_text",mItemListRect,std::string());
|
|
textBox->setBorderVisible(false);
|
|
textBox->setFollows(FOLLOWS_ALL);
|
|
textBox->setFontShadow(LLFontGL::NO_SHADOW);
|
|
textBox->setColor(LLUI::sColorsGroup->getColor("DefaultListText"));
|
|
addChild(textBox);
|
|
}
|
|
|
|
S32 LLScrollListCtrl::getSearchColumn()
|
|
{
|
|
// search for proper search column
|
|
if (mSearchColumn < 0)
|
|
{
|
|
LLScrollListItem* itemp = getFirstData();
|
|
if (itemp)
|
|
{
|
|
for(S32 column = 0; column < getNumColumns(); column++)
|
|
{
|
|
LLScrollListCell* cell = itemp->getColumn(column);
|
|
if (cell && cell->isText())
|
|
{
|
|
mSearchColumn = column;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return llclamp(mSearchColumn, 0, getNumColumns());
|
|
}
|
|
|
|
LLScrollListCtrl::~LLScrollListCtrl()
|
|
{
|
|
delete mSortCallback;
|
|
|
|
std::for_each(mItemList.begin(), mItemList.end(), DeletePointer());
|
|
std::for_each(mColumns.begin(), mColumns.end(), DeletePairedPointer());
|
|
}
|
|
|
|
|
|
BOOL LLScrollListCtrl::setMaxItemCount(S32 max_count)
|
|
{
|
|
if (max_count >= getItemCount())
|
|
{
|
|
mMaxItemCount = max_count;
|
|
}
|
|
return (max_count == mMaxItemCount);
|
|
}
|
|
|
|
S32 LLScrollListCtrl::isEmpty() const
|
|
{
|
|
return mItemList.empty();
|
|
}
|
|
|
|
S32 LLScrollListCtrl::getItemCount() const
|
|
{
|
|
return mItemList.size();
|
|
}
|
|
|
|
// virtual LLScrolListInterface function (was deleteAllItems)
|
|
void LLScrollListCtrl::clearRows()
|
|
{
|
|
std::for_each(mItemList.begin(), mItemList.end(), DeletePointer());
|
|
mItemList.clear();
|
|
//mItemCount = 0;
|
|
|
|
// Scroll the bar back up to the top.
|
|
mScrollbar->setDocParams(0, 0);
|
|
|
|
mScrollLines = 0;
|
|
mLastSelected = NULL;
|
|
updateLayout();
|
|
mDirty = false;
|
|
}
|
|
|
|
|
|
LLScrollListItem* LLScrollListCtrl::getFirstSelected() const
|
|
{
|
|
if (!getCanSelect())
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
item_list::const_iterator iter;
|
|
for(iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
if (item->getSelected())
|
|
{
|
|
return item;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
std::vector<LLScrollListItem*> LLScrollListCtrl::getAllSelected() const
|
|
{
|
|
std::vector<LLScrollListItem*> ret;
|
|
|
|
if (!getCanSelect())
|
|
{
|
|
return ret;
|
|
}
|
|
|
|
item_list::const_iterator iter;
|
|
for(iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
if (item->getSelected())
|
|
{
|
|
ret.push_back(item);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
uuid_vec_t LLScrollListCtrl::getSelectedIDs()
|
|
{
|
|
uuid_vec_t ids;
|
|
if (!getCanSelect()) return ids;
|
|
|
|
for(const auto& item : mItemList)
|
|
{
|
|
if (item->getSelected()) ids.push_back(item->getUUID());
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
S32 LLScrollListCtrl::getNumSelected() const
|
|
{
|
|
if (!getCanSelect())
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
S32 numSelected = 0;
|
|
|
|
for(item_list::const_iterator iter = mItemList.begin(); iter != mItemList.end(); ++iter)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
if (item->getSelected())
|
|
{
|
|
++numSelected;
|
|
}
|
|
}
|
|
|
|
return numSelected;
|
|
}
|
|
|
|
S32 LLScrollListCtrl::getFirstSelectedIndex() const
|
|
{
|
|
if (!getCanSelect())
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
S32 CurSelectedIndex = 0;
|
|
|
|
// make sure sort is up to date before returning an index
|
|
updateSort();
|
|
|
|
item_list::const_iterator iter;
|
|
for (iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
if (item->getSelected())
|
|
{
|
|
return CurSelectedIndex;
|
|
}
|
|
CurSelectedIndex++;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
LLScrollListItem* LLScrollListCtrl::getFirstData() const
|
|
{
|
|
if (mItemList.size() == 0)
|
|
{
|
|
return NULL;
|
|
}
|
|
return mItemList[0];
|
|
}
|
|
|
|
LLScrollListItem* LLScrollListCtrl::getLastData() const
|
|
{
|
|
if (mItemList.size() == 0)
|
|
{
|
|
return NULL;
|
|
}
|
|
return mItemList[mItemList.size() - 1];
|
|
}
|
|
|
|
std::vector<LLScrollListItem*> LLScrollListCtrl::getAllData() const
|
|
{
|
|
std::vector<LLScrollListItem*> ret;
|
|
ret.reserve(mItemList.size()); //Optimization
|
|
item_list::const_iterator iter;
|
|
for(iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
ret.push_back(item);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
uuid_vec_t LLScrollListCtrl::getAllIDs() const
|
|
{
|
|
uuid_vec_t ret;
|
|
ret.reserve(mItemList.size()); //Optimization
|
|
for(const auto& item : mItemList)
|
|
{
|
|
ret.push_back(item->getUUID());
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// returns first matching item
|
|
LLScrollListItem* LLScrollListCtrl::getItem(const LLSD& sd) const
|
|
{
|
|
std::string string_val = sd.asString();
|
|
|
|
item_list::const_iterator iter;
|
|
for(iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
// assumes string representation is good enough for comparison
|
|
if (item->getValue().asString() == string_val)
|
|
{
|
|
return item;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
void LLScrollListCtrl::reshape( S32 width, S32 height, BOOL called_from_parent )
|
|
{
|
|
LLUICtrl::reshape( width, height, called_from_parent );
|
|
|
|
updateLayout();
|
|
}
|
|
|
|
void LLScrollListCtrl::updateLayout()
|
|
{
|
|
// reserve room for column headers, if needed
|
|
S32 heading_size = (mDisplayColumnHeaders ? mHeadingHeight : 0);
|
|
mItemListRect.setOriginAndSize(
|
|
mBorderThickness,
|
|
mBorderThickness,
|
|
getRect().getWidth() - 2 * mBorderThickness,
|
|
getRect().getHeight() - (2 * mBorderThickness ) - heading_size );
|
|
|
|
if (mCommentTextView == NULL)
|
|
{
|
|
mCommentTextView = getChildView("comment_text");
|
|
}
|
|
|
|
mCommentTextView->setShape(mItemListRect);
|
|
|
|
// how many lines of content in a single "page"
|
|
S32 page_lines = getLinesPerPage();
|
|
|
|
BOOL scrollbar_visible = mLineHeight * getItemCount() > mItemListRect.getHeight();
|
|
if (scrollbar_visible)
|
|
{
|
|
// provide space on the right for scrollbar
|
|
mItemListRect.mRight = getRect().getWidth() - mBorderThickness - SCROLLBAR_SIZE;
|
|
}
|
|
|
|
mScrollbar->setOrigin(getRect().getWidth() - mBorderThickness - SCROLLBAR_SIZE, mItemListRect.mBottom);
|
|
mScrollbar->reshape(SCROLLBAR_SIZE, mItemListRect.getHeight() + (mDisplayColumnHeaders ? mHeadingHeight : 0));
|
|
mScrollbar->setPageSize(page_lines);
|
|
mScrollbar->setDocSize( getItemCount() );
|
|
mScrollbar->setVisible(scrollbar_visible);
|
|
|
|
dirtyColumns();
|
|
}
|
|
|
|
// Attempt to size the control to show all items.
|
|
// Do not make larger than width or height.
|
|
void LLScrollListCtrl::fitContents(S32 max_width, S32 max_height)
|
|
{
|
|
S32 height = llmin( getRequiredRect().getHeight(), max_height );
|
|
if(mPageLines)
|
|
height = llmin( mPageLines * mLineHeight + 2*mBorderThickness + (mDisplayColumnHeaders ? mHeadingHeight : 0), height );
|
|
|
|
S32 width = getRect().getWidth();
|
|
|
|
reshape( width, height );
|
|
}
|
|
|
|
|
|
LLRect LLScrollListCtrl::getRequiredRect()
|
|
{
|
|
S32 heading_size = (mDisplayColumnHeaders ? mHeadingHeight : 0);
|
|
S32 height = (mLineHeight * getItemCount())
|
|
+ (2 * mBorderThickness )
|
|
+ heading_size;
|
|
S32 width = getRect().getWidth();
|
|
|
|
return LLRect(0, height, width, 0);
|
|
}
|
|
|
|
|
|
BOOL LLScrollListCtrl::addItem( LLScrollListItem* item, EAddPosition pos, BOOL requires_column )
|
|
{
|
|
BOOL not_too_big = getItemCount() < mMaxItemCount;
|
|
if (not_too_big)
|
|
{
|
|
if (!mFilter.empty()) filterItem(item);
|
|
|
|
switch( pos )
|
|
{
|
|
case ADD_TOP:
|
|
mItemList.push_front(item);
|
|
setNeedsSort();
|
|
break;
|
|
|
|
case ADD_SORTED:
|
|
{
|
|
mItemList.push_back(item);
|
|
// std::stable_sort is expensive. Only do this if the user sort criteria is not column 0, otherwise
|
|
// setNeedsSort does what we want.
|
|
if (mSortColumns.empty() || mSortColumns[0].first != 0)
|
|
{
|
|
// sort by column 0, in ascending order
|
|
std::stable_sort(mItemList.begin(), mItemList.end(), SortScrollListItem({ {0,true} }, mSortCallback));
|
|
}
|
|
|
|
// ADD_SORTED just sorts by first column...
|
|
// this might not match user sort criteria, so flag list as being in unsorted state
|
|
setNeedsSort();
|
|
break;
|
|
}
|
|
case ADD_BOTTOM:
|
|
mItemList.push_back(item);
|
|
setNeedsSort();
|
|
break;
|
|
|
|
default:
|
|
llassert(0);
|
|
mItemList.push_back(item);
|
|
setNeedsSort();
|
|
break;
|
|
}
|
|
|
|
// create new column on demand
|
|
if (mColumns.empty() && requires_column)
|
|
{
|
|
LLScrollListColumn::Params col_params;
|
|
col_params.name = "default_column";
|
|
col_params.header.label = "";
|
|
col_params.width.dynamic_width = true;
|
|
addColumn(col_params);
|
|
}
|
|
|
|
S32 num_cols = item->getNumColumns();
|
|
S32 i = 0;
|
|
for (LLScrollListCell* cell = item->getColumn(i); i < num_cols; cell = item->getColumn(++i))
|
|
{
|
|
if (i >= (S32)mColumnsIndexed.size()) break;
|
|
|
|
cell->setWidth(mColumnsIndexed[i]->getWidth());
|
|
}
|
|
|
|
updateLineHeightInsert(item);
|
|
|
|
updateLayout();
|
|
}
|
|
|
|
return not_too_big;
|
|
}
|
|
|
|
// NOTE: This is *very* expensive for large lists, especially when we are dirtying the list every frame
|
|
// while receiving a long list of names.
|
|
// *TODO: Use bookkeeping to make this an incramental cost with item additions
|
|
S32 LLScrollListCtrl::calcMaxContentWidth()
|
|
{
|
|
const S32 HEADING_TEXT_PADDING = 25;
|
|
const S32 COLUMN_TEXT_PADDING = 10;
|
|
|
|
S32 max_item_width = 0;
|
|
|
|
ordered_columns_t::iterator column_itor;
|
|
for (column_itor = mColumnsIndexed.begin(); column_itor != mColumnsIndexed.end(); ++column_itor)
|
|
{
|
|
LLScrollListColumn* column = *column_itor;
|
|
if (!column) continue;
|
|
|
|
if (mColumnWidthsDirty)
|
|
{
|
|
// update max content width for this column, by looking at all items
|
|
column->mMaxContentWidth = column->mHeader ? LLFontGL::getFontSansSerifSmall()->getWidth(column->mLabel.getWString()) + mColumnPadding + HEADING_TEXT_PADDING : 0;
|
|
for (auto& item : mItemList)
|
|
{
|
|
if (item->getFiltered()) continue;
|
|
|
|
LLScrollListCell* cellp = item->getColumn(column->mIndex);
|
|
if (!cellp) continue;
|
|
|
|
column->mMaxContentWidth = llmax(LLFontGL::getFontSansSerifSmall()->getWidth(cellp->getValue().asString()) + mColumnPadding + COLUMN_TEXT_PADDING, column->mMaxContentWidth);
|
|
}
|
|
}
|
|
max_item_width += column->mMaxContentWidth;
|
|
}
|
|
mColumnWidthsDirty = false;
|
|
|
|
return max_item_width;
|
|
}
|
|
|
|
bool LLScrollListCtrl::updateColumnWidths()
|
|
{
|
|
bool width_changed = false;
|
|
ordered_columns_t::iterator column_itor;
|
|
for (column_itor = mColumnsIndexed.begin(); column_itor != mColumnsIndexed.end(); ++column_itor)
|
|
{
|
|
LLScrollListColumn* column = *column_itor;
|
|
if (!column) continue;
|
|
|
|
// update column width
|
|
S32 new_width = 0;
|
|
if (column->mRelWidth >= 0)
|
|
{
|
|
new_width = (S32)ll_round(column->mRelWidth*mItemListRect.getWidth());
|
|
}
|
|
else if (column->mDynamicWidth)
|
|
{
|
|
new_width = (mItemListRect.getWidth() - mTotalStaticColumnWidth - mTotalColumnPadding) / mNumDynamicWidthColumns;
|
|
}
|
|
else
|
|
{
|
|
new_width = column->getWidth();
|
|
}
|
|
|
|
if (column->getWidth() != new_width)
|
|
{
|
|
column->setWidth(new_width);
|
|
width_changed = true;
|
|
}
|
|
}
|
|
calcMaxContentWidth();
|
|
return width_changed;
|
|
}
|
|
|
|
const S32 SCROLL_LIST_ROW_PAD = 2;
|
|
|
|
// Line height is the max height of all the cells in all the items.
|
|
void LLScrollListCtrl::updateLineHeight()
|
|
{
|
|
mLineHeight = 0;
|
|
for (auto& itemp : mItemList)
|
|
{
|
|
S32 num_cols = itemp->getNumColumns();
|
|
S32 i = 0;
|
|
for (const LLScrollListCell* cell = itemp->getColumn(i); i < num_cols; cell = itemp->getColumn(++i))
|
|
{
|
|
mLineHeight = llmax( mLineHeight, cell->getHeight() + SCROLL_LIST_ROW_PAD );
|
|
}
|
|
}
|
|
}
|
|
|
|
// when the only change to line height is from an insert, we needn't scan the entire list
|
|
void LLScrollListCtrl::updateLineHeightInsert(LLScrollListItem* itemp)
|
|
{
|
|
S32 num_cols = itemp->getNumColumns();
|
|
S32 i = 0;
|
|
for (const LLScrollListCell* cell = itemp->getColumn(i); i < num_cols; cell = itemp->getColumn(++i))
|
|
{
|
|
mLineHeight = llmax( mLineHeight, cell->getHeight() + SCROLL_LIST_ROW_PAD );
|
|
}
|
|
}
|
|
|
|
|
|
void LLScrollListCtrl::updateColumns(bool force_update)
|
|
{
|
|
if (!mColumnsDirty && !force_update)
|
|
return;
|
|
|
|
mColumnsDirty = false;
|
|
|
|
bool columns_changed_width = updateColumnWidths();
|
|
|
|
// update column headers
|
|
std::vector<LLScrollListColumn*>::iterator column_ordered_it;
|
|
S32 left = mItemListRect.mLeft;
|
|
LLScrollColumnHeader* last_header = NULL;
|
|
for (column_ordered_it = mColumnsIndexed.begin(); column_ordered_it != mColumnsIndexed.end(); ++column_ordered_it)
|
|
{
|
|
if ((*column_ordered_it)->getWidth() < 0)
|
|
{
|
|
// skip hidden columns
|
|
continue;
|
|
}
|
|
LLScrollListColumn* column = *column_ordered_it;
|
|
|
|
if (column->mHeader)
|
|
{
|
|
column->mHeader->updateResizeBars();
|
|
|
|
last_header = column->mHeader;
|
|
S32 top = mItemListRect.mTop;
|
|
S32 right = left + column->getWidth();
|
|
|
|
if (column->mIndex != (S32)mColumnsIndexed.size()-1)
|
|
{
|
|
right += mColumnPadding;
|
|
}
|
|
right = llmax(left, llmin(mItemListRect.getWidth(), right));
|
|
S32 header_width = right - left;
|
|
|
|
last_header->reshape(header_width, mHeadingHeight);
|
|
last_header->translate(
|
|
left - last_header->getRect().mLeft,
|
|
top - last_header->getRect().mBottom);
|
|
last_header->setVisible(mDisplayColumnHeaders && header_width > 0);
|
|
left = right;
|
|
}
|
|
}
|
|
|
|
// expand last column header we encountered to full list width
|
|
if (last_header && last_header->canResize())
|
|
{
|
|
S32 new_width = llmax(0, mItemListRect.mRight - last_header->getRect().mLeft);
|
|
last_header->reshape(new_width, last_header->getRect().getHeight());
|
|
last_header->setVisible(mDisplayColumnHeaders && new_width > 0);
|
|
last_header->getColumn()->setWidth(new_width);
|
|
}
|
|
|
|
// propagate column widths to individual cells
|
|
if (columns_changed_width || force_update)
|
|
{
|
|
item_list::iterator iter;
|
|
for (iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem *itemp = *iter;
|
|
S32 num_cols = itemp->getNumColumns();
|
|
S32 i = 0;
|
|
for (LLScrollListCell* cell = itemp->getColumn(i); i < num_cols; cell = itemp->getColumn(++i))
|
|
{
|
|
if (i >= (S32)mColumnsIndexed.size()) break;
|
|
|
|
cell->setWidth(mColumnsIndexed[i]->getWidth());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void LLScrollListCtrl::setHeadingHeight(S32 heading_height)
|
|
{
|
|
mHeadingHeight = heading_height;
|
|
|
|
updateLayout();
|
|
|
|
}
|
|
void LLScrollListCtrl::setPageLines(S32 new_page_lines)
|
|
{
|
|
mPageLines = new_page_lines;
|
|
|
|
updateLayout();
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::selectFirstItem()
|
|
{
|
|
BOOL success = FALSE;
|
|
|
|
// our $%&@#$()^%#$()*^ iterators don't let us check against the first item inside out iteration
|
|
BOOL first_item = TRUE;
|
|
|
|
item_list::iterator iter;
|
|
for (iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem *itemp = *iter;
|
|
if (itemp->getFiltered()) continue;
|
|
if( first_item && itemp->getEnabled() )
|
|
{
|
|
if (!itemp->getSelected())
|
|
{
|
|
selectItem(itemp);
|
|
}
|
|
success = TRUE;
|
|
mOriginalSelection = 0;
|
|
}
|
|
else
|
|
{
|
|
deselectItem(itemp);
|
|
}
|
|
first_item = false;
|
|
}
|
|
if (mCommitOnSelectionChange)
|
|
{
|
|
commitIfChanged();
|
|
}
|
|
return success;
|
|
}
|
|
|
|
// Deselects all other items
|
|
// virtual
|
|
BOOL LLScrollListCtrl::selectNthItem( S32 target_index )
|
|
{
|
|
return selectItemRange(target_index, target_index);
|
|
}
|
|
|
|
// virtual
|
|
BOOL LLScrollListCtrl::selectItemRange( S32 first_index, S32 last_index )
|
|
{
|
|
if (mItemList.empty())
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
// make sure sort is up to date
|
|
updateSort();
|
|
|
|
S32 listlen = (S32)mItemList.size();
|
|
first_index = llclamp(first_index, 0, listlen-1);
|
|
|
|
if (last_index < 0)
|
|
last_index = listlen-1;
|
|
else
|
|
last_index = llclamp(last_index, first_index, listlen-1);
|
|
|
|
BOOL success = FALSE;
|
|
S32 index = 0;
|
|
for (item_list::iterator iter = mItemList.begin(); iter != mItemList.end(); )
|
|
{
|
|
LLScrollListItem *itemp = *iter;
|
|
if(!itemp)
|
|
{
|
|
iter = mItemList.erase(iter);
|
|
continue ;
|
|
}
|
|
|
|
if (itemp->getFiltered())
|
|
{
|
|
if (index >= first_index && index <= last_index)
|
|
{
|
|
if (itemp->getEnabled())
|
|
{
|
|
selectItem(itemp, FALSE);
|
|
success = TRUE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
deselectItem(itemp);
|
|
}
|
|
index++;
|
|
}
|
|
iter++ ;
|
|
}
|
|
|
|
if (mCommitOnSelectionChange)
|
|
{
|
|
commitIfChanged();
|
|
}
|
|
|
|
mSearchString.clear();
|
|
|
|
return success;
|
|
}
|
|
|
|
|
|
void LLScrollListCtrl::swapWithNext(S32 index)
|
|
{
|
|
if (index >= ((S32)mItemList.size() - 1))
|
|
{
|
|
// At end of list, doesn't do anything
|
|
return;
|
|
}
|
|
updateSort();
|
|
LLScrollListItem *cur_itemp = mItemList[index];
|
|
mItemList[index] = mItemList[index + 1];
|
|
mItemList[index + 1] = cur_itemp;
|
|
}
|
|
|
|
|
|
void LLScrollListCtrl::swapWithPrevious(S32 index)
|
|
{
|
|
if (index <= 0)
|
|
{
|
|
// At beginning of list, don't do anything
|
|
return;
|
|
}
|
|
|
|
updateSort();
|
|
LLScrollListItem *cur_itemp = mItemList[index];
|
|
mItemList[index] = mItemList[index - 1];
|
|
mItemList[index - 1] = cur_itemp;
|
|
}
|
|
|
|
void LLScrollListCtrl::moveToFront(S32 index)
|
|
{
|
|
if(index == 0 || index >= (S32)mItemList.size())
|
|
{
|
|
return;
|
|
}
|
|
|
|
LLScrollListCtrl::item_list::iterator it = mItemList.begin();
|
|
std::advance(it,index);
|
|
mItemList.push_front(*it);
|
|
mItemList.erase(it);
|
|
}
|
|
|
|
void LLScrollListCtrl::deleteSingleItem(S32 target_index)
|
|
{
|
|
if (target_index < 0 || target_index >= (S32)mItemList.size())
|
|
{
|
|
return;
|
|
}
|
|
|
|
updateSort();
|
|
|
|
LLScrollListItem *itemp;
|
|
itemp = mItemList[target_index];
|
|
if (itemp == mLastSelected)
|
|
{
|
|
mLastSelected = NULL;
|
|
}
|
|
delete itemp;
|
|
mItemList.erase(mItemList.begin() + target_index);
|
|
dirtyColumns();
|
|
}
|
|
|
|
//FIXME: refactor item deletion
|
|
void LLScrollListCtrl::deleteItems(const LLSD& sd)
|
|
{
|
|
item_list::iterator iter;
|
|
for (iter = mItemList.begin(); iter < mItemList.end(); )
|
|
{
|
|
LLScrollListItem* itemp = *iter;
|
|
if (itemp->getValue().asString() == sd.asString())
|
|
{
|
|
if (itemp == mLastSelected)
|
|
{
|
|
mLastSelected = NULL;
|
|
}
|
|
delete itemp;
|
|
iter = mItemList.erase(iter);
|
|
}
|
|
else
|
|
{
|
|
iter++;
|
|
}
|
|
}
|
|
|
|
dirtyColumns();
|
|
}
|
|
|
|
void LLScrollListCtrl::deleteSelectedItems()
|
|
{
|
|
item_list::iterator iter;
|
|
for (iter = mItemList.begin(); iter < mItemList.end(); )
|
|
{
|
|
LLScrollListItem* itemp = *iter;
|
|
if (itemp->getSelected())
|
|
{
|
|
delete itemp;
|
|
iter = mItemList.erase(iter);
|
|
}
|
|
else
|
|
{
|
|
iter++;
|
|
}
|
|
}
|
|
mLastSelected = NULL;
|
|
dirtyColumns();
|
|
}
|
|
|
|
void LLScrollListCtrl::mouseOverHighlightNthItem(S32 target_index)
|
|
{
|
|
if (mHighlightedItem != target_index)
|
|
{
|
|
mHighlightedItem = target_index;
|
|
}
|
|
}
|
|
|
|
S32 LLScrollListCtrl::selectMultiple( uuid_vec_t ids )
|
|
{
|
|
item_list::iterator iter;
|
|
S32 count = 0;
|
|
for (iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
if (item->getFiltered()) continue;
|
|
|
|
uuid_vec_t::iterator iditr;
|
|
for(iditr = ids.begin(); iditr != ids.end(); ++iditr)
|
|
{
|
|
if (item->getEnabled() && (item->getUUID() == (*iditr)))
|
|
{
|
|
selectItem(item,FALSE);
|
|
++count;
|
|
break;
|
|
}
|
|
}
|
|
if(ids.end() != iditr) ids.erase(iditr);
|
|
}
|
|
|
|
if (mCommitOnSelectionChange)
|
|
{
|
|
commitIfChanged();
|
|
}
|
|
return count;
|
|
}
|
|
|
|
S32 LLScrollListCtrl::getItemIndex( LLScrollListItem* target_item ) const
|
|
{
|
|
updateSort();
|
|
|
|
S32 index = 0;
|
|
for (LLScrollListItem* itemp : mItemList)
|
|
{
|
|
if (itemp->getFiltered()) continue;
|
|
if (target_item == itemp)
|
|
{
|
|
return index;
|
|
}
|
|
++index;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
S32 LLScrollListCtrl::getItemIndex( const LLUUID& target_id ) const
|
|
{
|
|
updateSort();
|
|
|
|
S32 index = 0;
|
|
for (LLScrollListItem* itemp : mItemList)
|
|
{
|
|
if (itemp->getFiltered()) continue;
|
|
if (target_id == itemp->getUUID())
|
|
{
|
|
return index;
|
|
}
|
|
++index;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void LLScrollListCtrl::selectPrevItem( BOOL extend_selection)
|
|
{
|
|
LLScrollListItem* prev_item = NULL;
|
|
|
|
if (!getFirstSelected())
|
|
{
|
|
// select last item
|
|
selectNthItem(getItemCount() - 1);
|
|
}
|
|
else
|
|
{
|
|
updateSort();
|
|
|
|
item_list::iterator iter;
|
|
for (iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem* cur_item = *iter;
|
|
if (cur_item->getFiltered()) continue;
|
|
|
|
if (cur_item->getSelected())
|
|
{
|
|
if (prev_item)
|
|
{
|
|
selectItem(prev_item, !extend_selection);
|
|
}
|
|
else
|
|
{
|
|
reportInvalidInput();
|
|
}
|
|
break;
|
|
}
|
|
|
|
// don't allow navigation to disabled elements
|
|
prev_item = cur_item->getEnabled() ? cur_item : prev_item;
|
|
}
|
|
}
|
|
|
|
if ((mCommitOnSelectionChange || mCommitOnKeyboardMovement))
|
|
{
|
|
commitIfChanged();
|
|
}
|
|
|
|
mSearchString.clear();
|
|
}
|
|
|
|
|
|
void LLScrollListCtrl::selectNextItem( BOOL extend_selection)
|
|
{
|
|
LLScrollListItem* next_item = NULL;
|
|
|
|
if (!getFirstSelected())
|
|
{
|
|
selectFirstItem();
|
|
}
|
|
else
|
|
{
|
|
updateSort();
|
|
|
|
item_list::reverse_iterator iter;
|
|
for (iter = mItemList.rbegin(); iter != mItemList.rend(); iter++)
|
|
{
|
|
LLScrollListItem* cur_item = *iter;
|
|
if (cur_item->getFiltered()) continue;
|
|
|
|
if (cur_item->getSelected())
|
|
{
|
|
if (next_item)
|
|
{
|
|
selectItem(next_item, !extend_selection);
|
|
}
|
|
else
|
|
{
|
|
reportInvalidInput();
|
|
}
|
|
break;
|
|
}
|
|
|
|
// don't allow navigation to disabled items
|
|
next_item = cur_item->getEnabled() ? cur_item : next_item;
|
|
}
|
|
}
|
|
|
|
if (mCommitOnKeyboardMovement)
|
|
{
|
|
onCommit();
|
|
}
|
|
|
|
mSearchString.clear();
|
|
}
|
|
|
|
|
|
|
|
void LLScrollListCtrl::deselectAllItems(BOOL no_commit_on_change)
|
|
{
|
|
item_list::iterator iter;
|
|
for (iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
deselectItem(item);
|
|
}
|
|
|
|
if (mCommitOnSelectionChange && !no_commit_on_change)
|
|
{
|
|
commitIfChanged();
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Use this to add comment text such as "Searching", which ignores column settings of list
|
|
|
|
void LLScrollListCtrl::setCommentText(const std::string& comment_text)
|
|
{
|
|
getChild<LLTextBox>("comment_text")->setWrappedText(comment_text);
|
|
}
|
|
|
|
LLScrollListItem* LLScrollListCtrl::addSeparator(EAddPosition pos)
|
|
{
|
|
LLScrollListItem::Params separator_params;
|
|
separator_params.enabled(false);
|
|
LLScrollListCell::Params column_params;
|
|
column_params.type = "icon";
|
|
column_params.value = "menu_separator.png";
|
|
column_params.color = LLColor4(0.f, 0.f, 0.f, 0.7f);
|
|
column_params.font_halign = LLFontGL::HCENTER;
|
|
separator_params.columns.add(column_params);
|
|
return addRow( separator_params, pos );
|
|
}
|
|
|
|
// Selects first enabled item of the given name.
|
|
// Returns false if item not found.
|
|
// Calls getItemByLabel in order to combine functionality
|
|
BOOL LLScrollListCtrl::selectItemByLabel(const std::string& label, BOOL case_sensitive)
|
|
{
|
|
deselectAllItems(TRUE); // ensure that no stale items are selected, even if we don't find a match
|
|
LLScrollListItem* item = getItemByLabel(label, case_sensitive);
|
|
|
|
bool found = NULL != item;
|
|
if(found)
|
|
{
|
|
selectItem(item);
|
|
}
|
|
|
|
if (mCommitOnSelectionChange)
|
|
{
|
|
commitIfChanged();
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
LLScrollListItem* LLScrollListCtrl::getItemByLabel(const std::string& label, BOOL case_sensitive, S32 column)
|
|
{
|
|
if (label.empty()) //RN: assume no empty items
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
std::string target_text = label;
|
|
if (!case_sensitive)
|
|
{
|
|
LLStringUtil::toLower(target_text);
|
|
}
|
|
|
|
item_list::iterator iter;
|
|
for (iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
if (item->getFiltered()) continue;
|
|
|
|
std::string item_text = item->getColumn(column)->getValue().asString(); // Only select enabled items with matching names
|
|
if (!case_sensitive)
|
|
{
|
|
LLStringUtil::toLower(item_text);
|
|
}
|
|
if(item_text == target_text)
|
|
{
|
|
return item;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
BOOL LLScrollListCtrl::selectItemByPrefix(const std::string& target, BOOL case_sensitive)
|
|
{
|
|
return selectItemByPrefix(utf8str_to_wstring(target), case_sensitive);
|
|
}
|
|
|
|
// Selects first enabled item that has a name where the name's first part matched the target string.
|
|
// Returns false if item not found.
|
|
BOOL LLScrollListCtrl::selectItemByPrefix(const LLWString& target, BOOL case_sensitive)
|
|
{
|
|
BOOL found = FALSE;
|
|
|
|
LLWString target_trimmed( target );
|
|
S32 target_len = target_trimmed.size();
|
|
|
|
if( 0 == target_len )
|
|
{
|
|
// Is "" a valid choice?
|
|
item_list::iterator iter;
|
|
for (iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
if (item->getFiltered()) continue;
|
|
|
|
// Only select enabled items with matching names
|
|
LLScrollListCell* cellp = item->getColumn(getSearchColumn());
|
|
BOOL select = cellp ? item->getEnabled() && ('\0' == cellp->getValue().asString()[0]) : FALSE;
|
|
if (select)
|
|
{
|
|
selectItem(item);
|
|
found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!case_sensitive)
|
|
{
|
|
// do comparisons in lower case
|
|
LLWStringUtil::toLower(target_trimmed);
|
|
}
|
|
|
|
for (item_list::iterator iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
|
|
// Don't select filtered items
|
|
if (item->getFiltered()) continue;
|
|
|
|
// Only select enabled items with matching names
|
|
LLScrollListCell* cellp = item->getColumn(getSearchColumn());
|
|
if (!cellp)
|
|
{
|
|
continue;
|
|
}
|
|
LLWString item_label = utf8str_to_wstring(cellp->getValue().asString());
|
|
if (!case_sensitive)
|
|
{
|
|
LLWStringUtil::toLower(item_label);
|
|
}
|
|
// remove extraneous whitespace from searchable label
|
|
LLWString trimmed_label = item_label;
|
|
LLWStringUtil::trim(trimmed_label);
|
|
|
|
BOOL select = item->getEnabled() && trimmed_label.compare(0, target_trimmed.size(), target_trimmed) == 0;
|
|
|
|
if (select)
|
|
{
|
|
// find offset of matching text (might have leading whitespace)
|
|
S32 offset = item_label.find(target_trimmed);
|
|
cellp->highlightText(offset, target_trimmed.size());
|
|
selectItem(item);
|
|
found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mCommitOnSelectionChange)
|
|
{
|
|
commitIfChanged();
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
const std::string LLScrollListCtrl::getSelectedItemLabel(S32 column) const
|
|
{
|
|
LLScrollListItem* item;
|
|
|
|
item = getFirstSelected();
|
|
if (item)
|
|
{
|
|
return item->getColumn(column)->getValue().asString();
|
|
}
|
|
|
|
return LLStringUtil::null;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// "StringUUID" interface: use this when you're creating a list that contains non-unique strings each of which
|
|
// has an associated, unique UUID, and only one of which can be selected at a time.
|
|
|
|
LLScrollListItem* LLScrollListCtrl::addStringUUIDItem(const std::string& item_text, const LLUUID& id, EAddPosition pos, BOOL enabled)
|
|
{
|
|
if (getItemCount() < mMaxItemCount)
|
|
{
|
|
LLScrollListItem::Params item_p;
|
|
item_p.enabled(enabled);
|
|
item_p.value(id);
|
|
item_p.columns.add().value(item_text).type("text");
|
|
|
|
return addRow( item_p, pos );
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Select the line or lines that match this UUID
|
|
BOOL LLScrollListCtrl::selectByID( const LLUUID& id )
|
|
{
|
|
return selectByValue( LLSD(id) );
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::setSelectedByValue(const LLSD& value, BOOL selected)
|
|
{
|
|
BOOL found = FALSE;
|
|
|
|
if (selected && !mAllowMultipleSelection) deselectAllItems(TRUE);
|
|
|
|
item_list::iterator iter;
|
|
for (iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
if (item->getFiltered()) continue;
|
|
|
|
if (item->getEnabled() && (item->getValue().asString() == value.asString()))
|
|
{
|
|
if (selected)
|
|
{
|
|
selectItem(item);
|
|
}
|
|
else
|
|
{
|
|
deselectItem(item);
|
|
}
|
|
found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (mCommitOnSelectionChange)
|
|
{
|
|
commitIfChanged();
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::isSelected(const LLSD& value) const
|
|
{
|
|
item_list::const_iterator iter;
|
|
for (iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
if (!item->getFiltered() && item->getValue().asString() == value.asString())
|
|
{
|
|
return item->getSelected();
|
|
}
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
LLUUID LLScrollListCtrl::getStringUUIDSelectedItem() const
|
|
{
|
|
LLScrollListItem* item = getFirstSelected();
|
|
|
|
if (item)
|
|
{
|
|
return item->getUUID();
|
|
}
|
|
|
|
return LLUUID::null;
|
|
}
|
|
|
|
LLSD LLScrollListCtrl::getSelectedValue()
|
|
{
|
|
LLScrollListItem* item = getFirstSelected();
|
|
|
|
if (item)
|
|
{
|
|
return item->getValue();
|
|
}
|
|
else
|
|
{
|
|
return LLSD();
|
|
}
|
|
}
|
|
|
|
void LLScrollListCtrl::drawItems()
|
|
{
|
|
S32 x = mItemListRect.mLeft;
|
|
S32 y = mItemListRect.mTop - mLineHeight;
|
|
|
|
// allow for partial line at bottom
|
|
S32 num_page_lines = getLinesPerPage();
|
|
|
|
LLRect item_rect;
|
|
|
|
LLGLSUIDefault gls_ui;
|
|
|
|
{
|
|
LLLocalClipRect clip(mItemListRect);
|
|
|
|
LLRect clip_rect = LLUI::getRootView()->getRect();
|
|
if (LLGLState<GL_SCISSOR_TEST>::isEnabled())
|
|
{
|
|
LLRect scissor = gGL.getScissor();
|
|
scissor.mLeft /= LLUI::getScaleFactor().mV[VX];
|
|
scissor.mTop /= LLUI::getScaleFactor().mV[VY];
|
|
scissor.mRight /= LLUI::getScaleFactor().mV[VX];
|
|
scissor.mBottom /= LLUI::getScaleFactor().mV[VY];
|
|
clip_rect.intersectWith(scissor);
|
|
}
|
|
|
|
S32 max_columns = 0;
|
|
|
|
LLColor4 highlight_color = LLColor4::white;
|
|
F32 type_ahead_timeout = LLUI::sConfigGroup->getF32("TypeAheadTimeout");
|
|
highlight_color.mV[VALPHA] = clamp_rescale(mSearchTimer.getElapsedTimeF32(), type_ahead_timeout * 0.7f, type_ahead_timeout, 0.4f, 0.f);
|
|
|
|
S32 first_line = mScrollLines;
|
|
S32 last_line = llmin((S32)mItemList.size() - 1, mScrollLines + getLinesPerPage());
|
|
|
|
if ((item_list::size_type)first_line >= mItemList.size())
|
|
{
|
|
return;
|
|
}
|
|
bool done = false;
|
|
for (S32 pass = 0; !done; ++pass)
|
|
{
|
|
bool should_continue = false; // False until all passes are done for all row cells.
|
|
S32 cur_y = y;
|
|
for (S32 index = first_line, line = first_line; index <= last_line; ++index)
|
|
{
|
|
LLScrollListItem* item = mItemList[index];
|
|
if (item->getFiltered()) continue; // Skip filtered
|
|
|
|
item_rect.setOriginAndSize(
|
|
x,
|
|
cur_y,
|
|
mItemListRect.getWidth(),
|
|
mLineHeight);
|
|
item->setRect(item_rect);
|
|
|
|
//LL_INFOS() << item_rect.getWidth() << LL_ENDL;
|
|
|
|
max_columns = llmax(max_columns, item->getNumColumns());
|
|
|
|
LLColor4 fg_color;
|
|
LLColor4 bg_color(LLColor4::transparent);
|
|
|
|
if (mScrollLines <= line && line < mScrollLines + num_page_lines)
|
|
{
|
|
cur_y -= mLineHeight;
|
|
|
|
// Do not draw if not on screen.
|
|
LLRect screen_rect = item_rect;
|
|
screen_rect.translate(LLFontGL::sCurOrigin.mX, LLFontGL::sCurOrigin.mY);
|
|
if (!clip_rect.overlaps(screen_rect))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
fg_color = (item->getEnabled() ? mFgUnselectedColor : mFgDisabledColor);
|
|
if (item->getSelected() && mCanSelect)
|
|
{
|
|
bg_color = mBgSelectedColor;
|
|
fg_color = (item->getEnabled() ? mFgSelectedColor : mFgDisabledColor);
|
|
}
|
|
else if (mHighlightedItem == line && mCanSelect)
|
|
{
|
|
bg_color = mHighlightedColor;
|
|
}
|
|
else
|
|
{
|
|
if (mDrawStripes && (line % 2 == 0) && (max_columns > 1))
|
|
{
|
|
bg_color = mBgStripeColor;
|
|
}
|
|
}
|
|
|
|
if (!item->getEnabled())
|
|
{
|
|
bg_color = mBgReadOnlyColor;
|
|
}
|
|
|
|
should_continue |= item->draw(pass, item_rect, fg_color, bg_color, highlight_color, mColumnPadding);
|
|
}
|
|
++line;
|
|
}
|
|
done = !should_continue;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
void LLScrollListCtrl::draw()
|
|
{
|
|
LLLocalClipRect clip(getLocalRect());
|
|
|
|
// if user specifies sort, make sure it is maintained
|
|
updateSort();
|
|
|
|
if (mNeedsScroll)
|
|
{
|
|
scrollToShowSelected();
|
|
mNeedsScroll = false;
|
|
}
|
|
LLRect background(0, getRect().getHeight(), getRect().getWidth(), 0);
|
|
// Draw background
|
|
if (mBackgroundVisible)
|
|
{
|
|
gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE);
|
|
gGL.color4fv( getEnabled() ? mBgWriteableColor.mV : mBgReadOnlyColor.mV );
|
|
gl_rect_2d(background);
|
|
}
|
|
|
|
updateColumns();
|
|
|
|
getChildView("comment_text")->setVisible(mItemList.empty());
|
|
|
|
drawItems();
|
|
|
|
if (mBorder)
|
|
{
|
|
mBorder->setKeyboardFocusHighlight(hasFocus());
|
|
}
|
|
|
|
LLUICtrl::draw();
|
|
}
|
|
|
|
void LLScrollListCtrl::setEnabled(BOOL enabled)
|
|
{
|
|
mCanSelect = enabled;
|
|
setTabStop(enabled);
|
|
mScrollbar->setTabStop(!enabled && mScrollbar->getPageSize() < mScrollbar->getDocSize());
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::handleScrollWheel(S32 x, S32 y, S32 clicks)
|
|
{
|
|
BOOL handled = FALSE;
|
|
// Pretend the mouse is over the scrollbar
|
|
handled = mScrollbar->handleScrollWheel( 0, 0, clicks );
|
|
|
|
if (mMouseWheelOpaque)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
return handled;
|
|
}
|
|
|
|
// *NOTE: Requires a valid row_index and column_index
|
|
LLRect LLScrollListCtrl::getCellRect(S32 row_index, S32 column_index)
|
|
{
|
|
LLRect cell_rect;
|
|
S32 rect_left = getColumnOffsetFromIndex(column_index) + mItemListRect.mLeft;
|
|
S32 rect_bottom = getRowOffsetFromIndex(row_index);
|
|
LLScrollListColumn* columnp = getColumn(column_index);
|
|
cell_rect.setOriginAndSize(rect_left, rect_bottom,
|
|
/*rect_left + */columnp->getWidth(), mLineHeight);
|
|
return cell_rect;
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::handleToolTip(S32 x, S32 y, std::string& msg, LLRect* sticky_rect_screen)
|
|
{
|
|
S32 column_index = getColumnIndexFromOffset(x);
|
|
LLScrollListColumn* columnp = getColumn(column_index);
|
|
|
|
if (columnp == NULL) return FALSE;
|
|
|
|
BOOL handled = FALSE;
|
|
// show tooltip for full name of hovered item if it has been truncated
|
|
LLScrollListItem* hit_item = hitItem(x, y);
|
|
if (hit_item)
|
|
{
|
|
LLScrollListCell* hit_cell = hit_item->getColumn(column_index);
|
|
if (!hit_cell) return FALSE;
|
|
if (hit_cell
|
|
//&& hit_cell->isText() // Singu Note: We welcome tooltips on any kind of cell
|
|
&& hit_cell->needsToolTip())
|
|
{
|
|
S32 row_index = getItemIndex(hit_item);
|
|
LLRect cell_rect = getCellRect(row_index, column_index);
|
|
// Convert rect local to screen coordinates
|
|
LLRect sticky_rect;
|
|
localRectToScreen(cell_rect, sticky_rect_screen);
|
|
|
|
msg = hit_cell->getToolTip();
|
|
}
|
|
handled = TRUE;
|
|
}
|
|
|
|
// otherwise, look for a tooltip associated with this column
|
|
LLScrollColumnHeader* headerp = columnp->mHeader;
|
|
if (headerp && !handled)
|
|
{
|
|
handled = headerp->handleToolTip(x, y, msg, sticky_rect_screen);
|
|
}
|
|
|
|
// Singu Note: If all else fails, try to use our own tooltip
|
|
if (!handled)
|
|
{
|
|
msg = LLUI::sShowXUINames ? getShowNamesToolTip() : getToolTip();
|
|
handled = !msg.empty();
|
|
}
|
|
|
|
return handled;
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::selectItemAt(S32 x, S32 y, MASK mask)
|
|
{
|
|
if (!mCanSelect) return FALSE;
|
|
|
|
BOOL selection_changed = FALSE;
|
|
|
|
LLScrollListItem* hit_item = hitItem(x, y);
|
|
|
|
if( hit_item )
|
|
{
|
|
if( mAllowMultipleSelection )
|
|
{
|
|
if (mask & MASK_SHIFT)
|
|
{
|
|
if (mLastSelected == NULL)
|
|
{
|
|
selectItem(hit_item);
|
|
}
|
|
else
|
|
{
|
|
// Select everthing between mLastSelected and hit_item
|
|
bool selecting = false;
|
|
item_list::iterator itor;
|
|
// If we multiselect backwards, we'll stomp on mLastSelected,
|
|
// meaning that we never stop selecting until hitting max or
|
|
// the end of the list.
|
|
LLScrollListItem* lastSelected = mLastSelected;
|
|
auto selected_count = getAllSelected().size();
|
|
for (itor = mItemList.begin(); itor != mItemList.end(); ++itor)
|
|
{
|
|
if(mMaxSelectable > 0 && selected_count >= mMaxSelectable)
|
|
{
|
|
if(mOnMaximumSelectCallback)
|
|
{
|
|
mOnMaximumSelectCallback();
|
|
}
|
|
break;
|
|
}
|
|
LLScrollListItem *item = *itor;
|
|
if (item->getFiltered()) continue;
|
|
if (item == hit_item || item == lastSelected)
|
|
{
|
|
selectItem(item, FALSE);
|
|
selecting = !selecting;
|
|
if (hit_item == lastSelected)
|
|
{
|
|
// stop selecting now, since we just clicked on our last selected item
|
|
selecting = FALSE;
|
|
}
|
|
}
|
|
if (selecting)
|
|
{
|
|
selectItem(item, FALSE);
|
|
}
|
|
++selected_count;
|
|
}
|
|
}
|
|
}
|
|
else if (mask & MASK_CONTROL)
|
|
{
|
|
if (hit_item->getSelected())
|
|
{
|
|
deselectItem(hit_item);
|
|
}
|
|
else
|
|
{
|
|
if(!(mMaxSelectable > 0 && getAllSelected().size() >= mMaxSelectable))
|
|
{
|
|
selectItem(hit_item, FALSE);
|
|
}
|
|
else
|
|
{
|
|
if(mOnMaximumSelectCallback)
|
|
{
|
|
mOnMaximumSelectCallback();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
deselectAllItems(TRUE);
|
|
selectItem(hit_item);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
selectItem(hit_item);
|
|
}
|
|
|
|
selection_changed = mSelectionChanged;
|
|
if (mCommitOnSelectionChange)
|
|
{
|
|
commitIfChanged();
|
|
}
|
|
|
|
// clear search string on mouse operations
|
|
mSearchString.clear();
|
|
}
|
|
else
|
|
{
|
|
//mLastSelected = NULL;
|
|
//deselectAllItems(TRUE);
|
|
}
|
|
|
|
return selection_changed;
|
|
}
|
|
|
|
|
|
BOOL LLScrollListCtrl::handleMouseDown(S32 x, S32 y, MASK mask)
|
|
{
|
|
BOOL handled = childrenHandleMouseDown(x, y, mask) != NULL;
|
|
|
|
if( !handled )
|
|
{
|
|
// set keyboard focus first, in case click action wants to move focus elsewhere
|
|
setFocus(TRUE);
|
|
|
|
// clear selection changed flag because user is starting a selection operation
|
|
mSelectionChanged = false;
|
|
|
|
handleClick(x, y, mask);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::handleMouseUp(S32 x, S32 y, MASK mask)
|
|
{
|
|
if (hasMouseCapture())
|
|
{
|
|
// release mouse capture immediately so
|
|
// scroll to show selected logic will work
|
|
gFocusMgr.setMouseCapture(NULL);
|
|
if(mask == MASK_NONE)
|
|
{
|
|
selectItemAt(x, y, mask);
|
|
mNeedsScroll = true;
|
|
}
|
|
}
|
|
|
|
// always commit when mouse operation is completed inside list
|
|
if (mItemListRect.pointInRect(x,y))
|
|
{
|
|
mDirty = mDirty || mSelectionChanged;
|
|
mSelectionChanged = false;
|
|
onCommit();
|
|
}
|
|
|
|
return LLUICtrl::handleMouseUp(x, y, mask);
|
|
}
|
|
|
|
// virtual
|
|
BOOL LLScrollListCtrl::handleRightMouseDown(S32 x, S32 y, MASK mask)
|
|
{
|
|
if (!mPopupMenu) return FALSE; // No menu, no bother
|
|
LLScrollListItem *item = hitItem(x, y);
|
|
if (item)
|
|
{
|
|
if (!item->getSelected()) selectItem(item); // Right click on unselected item is for that item only
|
|
}
|
|
else if (LLScrollListColumn* col = getColumn(getColumnIndexFromOffset(x)))
|
|
{
|
|
if (col->mHeader && col->mHeader->getRect().pointInRect(x,y)) // Right clicking a column header shouldn't bring up a menu
|
|
return FALSE;
|
|
}
|
|
gFocusMgr.setKeyboardFocus(this); // Menu listeners rely on this
|
|
mPopupMenu->buildDrawLabels();
|
|
mPopupMenu->updateParent(LLMenuGL::sMenuContainer);
|
|
LLMenuGL::showPopup(this, mPopupMenu, x, y);
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::handleDoubleClick(S32 x, S32 y, MASK mask)
|
|
{
|
|
//BOOL handled = FALSE;
|
|
BOOL handled = handleClick(x, y, mask);
|
|
|
|
if (!handled)
|
|
{
|
|
// Offer the click to the children, even if we aren't enabled
|
|
// so the scroll bars will work.
|
|
if (NULL == LLView::childrenHandleDoubleClick(x, y, mask))
|
|
{
|
|
// Run the callback only if an item is being double-clicked.
|
|
if( mCanSelect && hitItem(x, y) && mOnDoubleClickCallback )
|
|
{
|
|
mOnDoubleClickCallback();
|
|
}
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::handleClick(S32 x, S32 y, MASK mask)
|
|
{
|
|
// which row was clicked on?
|
|
LLScrollListItem* hit_item = hitItem(x, y);
|
|
if (!hit_item) return FALSE;
|
|
|
|
// get appropriate cell from that row
|
|
S32 column_index = getColumnIndexFromOffset(x);
|
|
LLScrollListCell* hit_cell = hit_item->getColumn(column_index);
|
|
if (!hit_cell) return FALSE;
|
|
|
|
// if cell handled click directly (i.e. clicked on an embedded checkbox)
|
|
if (hit_cell->handleClick())
|
|
{
|
|
// if item not currently selected, select it
|
|
if (!hit_item->getSelected())
|
|
{
|
|
selectItemAt(x, y, mask);
|
|
gFocusMgr.setMouseCapture(this);
|
|
mNeedsScroll = true;
|
|
}
|
|
|
|
// propagate state of cell to rest of selected column
|
|
{
|
|
// propagate value of this cell to other selected items
|
|
// and commit the respective widgets
|
|
LLSD item_value = hit_cell->getValue();
|
|
for (item_list::iterator iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
if (item->getSelected())
|
|
{
|
|
LLScrollListCell* cellp = item->getColumn(column_index);
|
|
cellp->setValue(item_value);
|
|
cellp->onCommit();
|
|
}
|
|
}
|
|
//FIXME: find a better way to signal cell changes
|
|
onCommit();
|
|
}
|
|
// eat click (e.g. do not trigger double click callback)
|
|
return TRUE;
|
|
}
|
|
else
|
|
{
|
|
// treat this as a normal single item selection
|
|
selectItemAt(x, y, mask);
|
|
gFocusMgr.setMouseCapture(this);
|
|
mNeedsScroll = true;
|
|
// do not eat click (allow double click callback)
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
LLScrollListItem* LLScrollListCtrl::hitItem( S32 x, S32 y )
|
|
{
|
|
// Excludes disabled items.
|
|
LLScrollListItem* hit_item = NULL;
|
|
|
|
updateSort();
|
|
|
|
LLRect item_rect;
|
|
item_rect.setLeftTopAndSize(
|
|
mItemListRect.mLeft,
|
|
mItemListRect.mTop,
|
|
mItemListRect.getWidth(),
|
|
mLineHeight );
|
|
|
|
// allow for partial line at bottom
|
|
S32 num_page_lines = getLinesPerPage();
|
|
|
|
S32 line = 0;
|
|
for(LLScrollListItem* item : mItemList)
|
|
{
|
|
if (item->getFiltered()) continue;
|
|
if( mScrollLines <= line && line < mScrollLines + num_page_lines )
|
|
{
|
|
if( item->getEnabled() && item_rect.pointInRect( x, y ) )
|
|
{
|
|
hit_item = item;
|
|
break;
|
|
}
|
|
|
|
item_rect.translate(0, -mLineHeight);
|
|
}
|
|
++line;
|
|
}
|
|
|
|
return hit_item;
|
|
}
|
|
|
|
S32 LLScrollListCtrl::getColumnIndexFromOffset(S32 x)
|
|
{
|
|
// which column did we hit?
|
|
S32 left = 0;
|
|
S32 right = 0;
|
|
S32 width = 0;
|
|
S32 column_index = 0;
|
|
|
|
ordered_columns_t::const_iterator iter = mColumnsIndexed.begin();
|
|
ordered_columns_t::const_iterator end = mColumnsIndexed.end();
|
|
for ( ; iter != end; ++iter)
|
|
{
|
|
width = (*iter)->getWidth() + mColumnPadding;
|
|
right += width;
|
|
if (left <= x && x < right )
|
|
{
|
|
break;
|
|
}
|
|
|
|
// set left for next column as right of current column
|
|
left = right;
|
|
column_index++;
|
|
}
|
|
|
|
return llclamp(column_index, 0, getNumColumns() - 1);
|
|
}
|
|
|
|
|
|
S32 LLScrollListCtrl::getColumnOffsetFromIndex(S32 index)
|
|
{
|
|
S32 column_offset = 0;
|
|
ordered_columns_t::const_iterator iter = mColumnsIndexed.begin();
|
|
ordered_columns_t::const_iterator end = mColumnsIndexed.end();
|
|
for ( ; iter != end; ++iter)
|
|
{
|
|
if (index-- <= 0)
|
|
{
|
|
return column_offset;
|
|
}
|
|
column_offset += (*iter)->getWidth() + mColumnPadding;
|
|
}
|
|
|
|
// when running off the end, return the rightmost pixel
|
|
return mItemListRect.mRight;
|
|
}
|
|
|
|
S32 LLScrollListCtrl::getRowOffsetFromIndex(S32 index)
|
|
{
|
|
S32 row_bottom = (mItemListRect.mTop - ((index - mScrollLines + 1) * mLineHeight) );
|
|
return row_bottom;
|
|
}
|
|
|
|
void LLScrollListCtrl::filterItem(LLScrollListItem* item)
|
|
{
|
|
for (const auto& column : item->mColumns)
|
|
{
|
|
// Only filter text, search tooltip because it'll usually be the text anyway.
|
|
if (column->isText() && boost::icontains(column->getToolTip(), mFilter))
|
|
{
|
|
item->setFiltered(false);
|
|
return;
|
|
}
|
|
}
|
|
item->setFiltered(true);
|
|
}
|
|
|
|
void LLScrollListCtrl::setFilter(const std::string& filter)
|
|
{
|
|
if (filter == mFilter) return;
|
|
|
|
bool no_filter = filter.empty();
|
|
// If our filter string has been expanded, we can skip already filtered items
|
|
bool expanded = !no_filter && !mFilter.empty() && boost::icontains(filter, mFilter);
|
|
|
|
mFilter = filter;
|
|
for (auto& item : mItemList)
|
|
{
|
|
if (no_filter) item->setFiltered(false);
|
|
else if (!expanded || !item->getFiltered())
|
|
{
|
|
filterItem(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
BOOL LLScrollListCtrl::handleHover(S32 x,S32 y,MASK mask)
|
|
{
|
|
BOOL handled = FALSE;
|
|
|
|
if (hasMouseCapture())
|
|
{
|
|
if(mask == MASK_NONE)
|
|
{
|
|
selectItemAt(x, y, mask);
|
|
mNeedsScroll = true;
|
|
}
|
|
}
|
|
else
|
|
if (mCanSelect)
|
|
{
|
|
LLScrollListItem* item = hitItem(x, y);
|
|
if (item)
|
|
{
|
|
mouseOverHighlightNthItem(getItemIndex(item));
|
|
}
|
|
else
|
|
{
|
|
mouseOverHighlightNthItem(-1);
|
|
}
|
|
}
|
|
|
|
handled = LLUICtrl::handleHover( x, y, mask );
|
|
|
|
return handled;
|
|
}
|
|
|
|
void LLScrollListCtrl::onMouseLeave(S32 x, S32 y, MASK mask)
|
|
{
|
|
// clear mouse highlight
|
|
mouseOverHighlightNthItem(-1);
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::handleKeyHere(KEY key,MASK mask )
|
|
{
|
|
BOOL handled = FALSE;
|
|
|
|
// not called from parent means we have keyboard focus or a child does
|
|
if (mCanSelect)
|
|
{
|
|
// Ignore capslock
|
|
//mask = mask; //Why was this here?
|
|
|
|
if (mask == MASK_NONE)
|
|
{
|
|
switch(key)
|
|
{
|
|
case KEY_UP:
|
|
if (mAllowKeyboardMovement || hasFocus())
|
|
{
|
|
// commit implicit in call
|
|
selectPrevItem(FALSE);
|
|
mNeedsScroll = true;
|
|
handled = TRUE;
|
|
}
|
|
break;
|
|
case KEY_DOWN:
|
|
if (mAllowKeyboardMovement || hasFocus())
|
|
{
|
|
// commit implicit in call
|
|
selectNextItem(FALSE);
|
|
mNeedsScroll = true;
|
|
handled = TRUE;
|
|
}
|
|
break;
|
|
case KEY_PAGE_UP:
|
|
if (mAllowKeyboardMovement || hasFocus())
|
|
{
|
|
selectNthItem(getFirstSelectedIndex() - (mScrollbar->getPageSize() - 1));
|
|
mNeedsScroll = true;
|
|
if (mCommitOnKeyboardMovement
|
|
&& !mCommitOnSelectionChange)
|
|
{
|
|
onCommit();
|
|
}
|
|
handled = TRUE;
|
|
}
|
|
break;
|
|
case KEY_PAGE_DOWN:
|
|
if (mAllowKeyboardMovement || hasFocus())
|
|
{
|
|
selectNthItem(getFirstSelectedIndex() + (mScrollbar->getPageSize() - 1));
|
|
mNeedsScroll = true;
|
|
if (mCommitOnKeyboardMovement
|
|
&& !mCommitOnSelectionChange)
|
|
{
|
|
onCommit();
|
|
}
|
|
handled = TRUE;
|
|
}
|
|
break;
|
|
case KEY_HOME:
|
|
if (mAllowKeyboardMovement || hasFocus())
|
|
{
|
|
selectFirstItem();
|
|
mNeedsScroll = true;
|
|
if (mCommitOnKeyboardMovement
|
|
&& !mCommitOnSelectionChange)
|
|
{
|
|
onCommit();
|
|
}
|
|
handled = TRUE;
|
|
}
|
|
break;
|
|
case KEY_END:
|
|
if (mAllowKeyboardMovement || hasFocus())
|
|
{
|
|
selectNthItem(getItemCount() - 1);
|
|
mNeedsScroll = true;
|
|
if (mCommitOnKeyboardMovement
|
|
&& !mCommitOnSelectionChange)
|
|
{
|
|
onCommit();
|
|
}
|
|
handled = TRUE;
|
|
}
|
|
break;
|
|
case KEY_RETURN:
|
|
// JC - Special case: Only claim to have handled it
|
|
// if we're the special non-commit-on-move
|
|
// type. AND we are visible
|
|
if (!mCommitOnKeyboardMovement && mask == MASK_NONE)
|
|
{
|
|
onCommit();
|
|
mSearchString.clear();
|
|
handled = TRUE;
|
|
}
|
|
break;
|
|
case KEY_BACKSPACE:
|
|
mSearchTimer.reset();
|
|
if (mSearchString.size())
|
|
{
|
|
mSearchString.erase(mSearchString.size() - 1, 1);
|
|
}
|
|
if (mSearchString.empty())
|
|
{
|
|
if (getFirstSelected())
|
|
{
|
|
LLScrollListCell* cellp = getFirstSelected()->getColumn(getSearchColumn());
|
|
if (cellp)
|
|
{
|
|
cellp->highlightText(0, 0);
|
|
}
|
|
}
|
|
}
|
|
else if (selectItemByPrefix(wstring_to_utf8str(mSearchString), FALSE))
|
|
{
|
|
mNeedsScroll = true;
|
|
// update search string only on successful match
|
|
mSearchTimer.reset();
|
|
|
|
if (mCommitOnKeyboardMovement
|
|
&& !mCommitOnSelectionChange)
|
|
{
|
|
onCommit();
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
// TODO: multiple: shift-up, shift-down, shift-home, shift-end, select all
|
|
}
|
|
|
|
return handled;
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::handleUnicodeCharHere(llwchar uni_char)
|
|
{
|
|
if ((uni_char < 0x20) || (uni_char == 0x7F)) // Control character or DEL
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
// perform incremental search based on keyboard input
|
|
if (mSearchTimer.getElapsedTimeF32() > LLUI::sConfigGroup->getF32("TypeAheadTimeout"))
|
|
{
|
|
mSearchString.clear();
|
|
}
|
|
|
|
// type ahead search is case insensitive
|
|
uni_char = LLStringOps::toLower((llwchar)uni_char);
|
|
|
|
if (selectItemByPrefix(wstring_to_utf8str(mSearchString + (llwchar)uni_char), FALSE))
|
|
{
|
|
// update search string only on successful match
|
|
mNeedsScroll = true;
|
|
mSearchString += uni_char;
|
|
mSearchTimer.reset();
|
|
|
|
if (mCommitOnKeyboardMovement
|
|
&& !mCommitOnSelectionChange)
|
|
{
|
|
onCommit();
|
|
}
|
|
}
|
|
// handle iterating over same starting character
|
|
else if (isRepeatedChars(mSearchString + (llwchar)uni_char) && !mItemList.empty())
|
|
{
|
|
// start from last selected item, in case we previously had a successful match against
|
|
// duplicated characters ('AA' matches 'Aaron')
|
|
item_list::iterator start_iter = mItemList.begin();
|
|
S32 first_selected = getFirstSelectedIndex();
|
|
|
|
// if we have a selection (> -1) then point iterator at the selected item
|
|
if (first_selected > 0)
|
|
{
|
|
// point iterator to first selected item
|
|
start_iter += first_selected;
|
|
}
|
|
|
|
// start search at first item after current selection
|
|
item_list::iterator iter = start_iter;
|
|
++iter;
|
|
if (iter == mItemList.end())
|
|
{
|
|
iter = mItemList.begin();
|
|
}
|
|
|
|
// loop around once, back to previous selection
|
|
while(iter != start_iter)
|
|
{
|
|
LLScrollListItem* item = *iter;
|
|
if (!item->getFiltered())
|
|
{
|
|
LLScrollListCell* cellp = item->getColumn(getSearchColumn());
|
|
if (cellp)
|
|
{
|
|
// Only select enabled items with matching first characters
|
|
LLWString item_label = utf8str_to_wstring(cellp->getValue().asString());
|
|
if (item->getEnabled() && LLStringOps::toLower(item_label[0]) == uni_char)
|
|
{
|
|
selectItem(item);
|
|
mNeedsScroll = true;
|
|
cellp->highlightText(0, 1);
|
|
mSearchTimer.reset();
|
|
|
|
if (mCommitOnKeyboardMovement
|
|
&& !mCommitOnSelectionChange)
|
|
{
|
|
onCommit();
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
++iter;
|
|
if (iter == mItemList.end())
|
|
{
|
|
iter = mItemList.begin();
|
|
}
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
void LLScrollListCtrl::reportInvalidInput()
|
|
{
|
|
make_ui_sound("UISndBadKeystroke");
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::isRepeatedChars(const LLWString& string) const
|
|
{
|
|
if (string.empty())
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
llwchar first_char = string[0];
|
|
|
|
for (U32 i = 0; i < string.size(); i++)
|
|
{
|
|
if (string[i] != first_char)
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
void LLScrollListCtrl::selectItem(LLScrollListItem* itemp, BOOL select_single_item)
|
|
{
|
|
if (!itemp) return;
|
|
|
|
if (!itemp->getSelected())
|
|
{
|
|
if (mLastSelected)
|
|
{
|
|
LLScrollListCell* cellp = mLastSelected->getColumn(getSearchColumn());
|
|
if (cellp)
|
|
{
|
|
cellp->highlightText(0, 0);
|
|
}
|
|
}
|
|
if (select_single_item)
|
|
{
|
|
deselectAllItems(TRUE);
|
|
}
|
|
itemp->setSelected(TRUE);
|
|
mLastSelected = itemp;
|
|
mSelectionChanged = true;
|
|
}
|
|
}
|
|
|
|
void LLScrollListCtrl::deselectItem(LLScrollListItem* itemp)
|
|
{
|
|
if (!itemp) return;
|
|
|
|
if (itemp->getSelected())
|
|
{
|
|
if (mLastSelected == itemp)
|
|
{
|
|
mLastSelected = NULL;
|
|
}
|
|
|
|
itemp->setSelected(FALSE);
|
|
LLScrollListCell* cellp = itemp->getColumn(getSearchColumn());
|
|
if (cellp)
|
|
{
|
|
cellp->highlightText(0, 0);
|
|
}
|
|
mSelectionChanged = true;
|
|
}
|
|
}
|
|
|
|
void LLScrollListCtrl::commitIfChanged()
|
|
{
|
|
if (mSelectionChanged)
|
|
{
|
|
mDirty = true;
|
|
mSelectionChanged = FALSE;
|
|
onCommit();
|
|
}
|
|
}
|
|
|
|
struct SameSortColumn
|
|
{
|
|
SameSortColumn(S32 column) : mColumn(column) {}
|
|
S32 mColumn;
|
|
|
|
bool operator()(LLScrollListCtrl::sort_column_t sort_column) { return sort_column.first == mColumn; }
|
|
};
|
|
|
|
BOOL LLScrollListCtrl::setSort(S32 column_idx, BOOL ascending)
|
|
{
|
|
LLScrollListColumn* sort_column = getColumn(column_idx);
|
|
if (!sort_column) return FALSE;
|
|
|
|
sort_column->mSortDirection = ascending ? LLScrollListColumn::ASCENDING : LLScrollListColumn::DESCENDING;
|
|
|
|
sort_column_t new_sort_column(column_idx, ascending);
|
|
|
|
setNeedsSort();
|
|
|
|
if (mSortColumns.empty())
|
|
{
|
|
mSortColumns.push_back(new_sort_column);
|
|
return TRUE;
|
|
}
|
|
else
|
|
{
|
|
// grab current sort column
|
|
sort_column_t cur_sort_column = mSortColumns.back();
|
|
|
|
// remove any existing sort criterion referencing this column
|
|
// and add the new one
|
|
mSortColumns.erase(remove_if(mSortColumns.begin(), mSortColumns.end(), SameSortColumn(column_idx)), mSortColumns.end());
|
|
mSortColumns.push_back(new_sort_column);
|
|
|
|
// did the sort criteria change?
|
|
return (cur_sort_column != new_sort_column);
|
|
}
|
|
}
|
|
|
|
void LLScrollListCtrl::setSortEnabled(bool sort)
|
|
{
|
|
bool update = sort && !mSortEnabled;
|
|
mSortEnabled = sort;
|
|
if (update)
|
|
{
|
|
updateSort();
|
|
}
|
|
}
|
|
|
|
S32 LLScrollListCtrl::getLinesPerPage()
|
|
{
|
|
//if mPageLines is NOT provided display all item
|
|
if(mPageLines)
|
|
{
|
|
return mPageLines;
|
|
}
|
|
else
|
|
{
|
|
return mLineHeight ? mItemListRect.getHeight() / mLineHeight : getItemCount();
|
|
}
|
|
}
|
|
|
|
|
|
// Called by scrollbar
|
|
void LLScrollListCtrl::onScrollChange( S32 new_pos, LLScrollbar* scrollbar )
|
|
{
|
|
mScrollLines = new_pos;
|
|
}
|
|
|
|
|
|
void LLScrollListCtrl::sortByColumn(const std::string& name, BOOL ascending)
|
|
{
|
|
column_map_t::iterator itor = mColumns.find(name);
|
|
if (itor != mColumns.end())
|
|
{
|
|
sortByColumnIndex((*itor).second->mIndex, ascending);
|
|
}
|
|
}
|
|
|
|
// First column is column 0
|
|
void LLScrollListCtrl::sortByColumnIndex(U32 column, BOOL ascending)
|
|
{
|
|
setSort(column, ascending);
|
|
updateSort();
|
|
}
|
|
|
|
void LLScrollListCtrl::updateSort() const
|
|
{
|
|
if (hasSortOrder() && !isSorted())
|
|
{
|
|
// do stable sort to preserve any previous sorts
|
|
std::stable_sort(
|
|
mItemList.begin(),
|
|
mItemList.end(),
|
|
SortScrollListItem(mSortColumns,mSortCallback));
|
|
|
|
mSorted = true;
|
|
}
|
|
}
|
|
|
|
// for one-shot sorts, does not save sort column/order
|
|
void LLScrollListCtrl::sortOnce(S32 column, BOOL ascending)
|
|
{
|
|
std::vector<sort_column_t > sort_column;
|
|
sort_column.push_back(std::make_pair(column, ascending));
|
|
|
|
// do stable sort to preserve any previous sorts
|
|
std::stable_sort(
|
|
mItemList.begin(),
|
|
mItemList.end(),
|
|
SortScrollListItem(sort_column,mSortCallback));
|
|
}
|
|
|
|
void LLScrollListCtrl::dirtyColumns()
|
|
{
|
|
mColumnsDirty = true;
|
|
mColumnWidthsDirty = true;
|
|
|
|
// need to keep mColumnsIndexed up to date
|
|
// just in case someone indexes into it immediately
|
|
mColumnsIndexed.resize(mColumns.size());
|
|
|
|
column_map_t::iterator column_itor;
|
|
for (column_itor = mColumns.begin(); column_itor != mColumns.end(); ++column_itor)
|
|
{
|
|
LLScrollListColumn *column = column_itor->second;
|
|
mColumnsIndexed[column_itor->second->mIndex] = column;
|
|
}
|
|
}
|
|
|
|
|
|
S32 LLScrollListCtrl::getScrollPos() const
|
|
{
|
|
return mScrollbar->getDocPos();
|
|
}
|
|
|
|
|
|
void LLScrollListCtrl::setScrollPos( S32 pos )
|
|
{
|
|
mScrollbar->setDocPos( pos );
|
|
|
|
onScrollChange(mScrollbar->getDocPos(), mScrollbar);
|
|
}
|
|
|
|
|
|
void LLScrollListCtrl::scrollToShowSelected()
|
|
{
|
|
// don't scroll automatically when capturing mouse input
|
|
// as that will change what is currently under the mouse cursor
|
|
if (hasMouseCapture())
|
|
{
|
|
return;
|
|
}
|
|
|
|
updateSort();
|
|
|
|
S32 index = getFirstSelectedIndex();
|
|
if (index < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LLScrollListItem* item = mItemList[index];
|
|
if (!item)
|
|
{
|
|
// I don't THINK this should ever happen.
|
|
return;
|
|
}
|
|
|
|
S32 lowest = mScrollLines;
|
|
S32 page_lines = getLinesPerPage();
|
|
S32 highest = mScrollLines + page_lines;
|
|
|
|
if (index < lowest)
|
|
{
|
|
// need to scroll to show item
|
|
setScrollPos(index);
|
|
}
|
|
else if (highest <= index)
|
|
{
|
|
setScrollPos(index - page_lines + 1);
|
|
}
|
|
}
|
|
|
|
void LLScrollListCtrl::updateStaticColumnWidth(LLScrollListColumn* col, S32 new_width)
|
|
{
|
|
mTotalStaticColumnWidth += llmax(0, new_width) - llmax(0, col->getWidth());
|
|
}
|
|
|
|
|
|
// virtual
|
|
LLXMLNodePtr LLScrollListCtrl::getXML(bool save_children) const
|
|
{
|
|
LLXMLNodePtr node = LLUICtrl::getXML();
|
|
node->setName(LL_SCROLL_LIST_CTRL_TAG);
|
|
|
|
// Attributes
|
|
node->createChild("multi_select", TRUE)->setBoolValue(mAllowMultipleSelection);
|
|
node->createChild("draw_border", TRUE)->setBoolValue((mBorder != NULL));
|
|
node->createChild("draw_heading", TRUE)->setBoolValue(mDisplayColumnHeaders);
|
|
node->createChild("background_visible", TRUE)->setBoolValue(mBackgroundVisible);
|
|
node->createChild("draw_stripes", TRUE)->setBoolValue(mDrawStripes);
|
|
node->createChild("column_padding", TRUE)->setIntValue(mColumnPadding);
|
|
node->createChild("mouse_wheel_opaque", TRUE)->setBoolValue(mMouseWheelOpaque);
|
|
addColorXML(node, mBgWriteableColor, "bg_writeable_color", "ScrollBgWriteableColor");
|
|
addColorXML(node, mBgReadOnlyColor, "bg_read_only_color", "ScrollBgReadOnlyColor");
|
|
addColorXML(node, mBgSelectedColor, "bg_selected_color", "ScrollSelectedBGColor");
|
|
addColorXML(node, mBgStripeColor, "bg_stripe_color", "ScrollBGStripeColor");
|
|
addColorXML(node, mFgSelectedColor, "fg_selected_color", "ScrollSelectedFGColor");
|
|
addColorXML(node, mFgUnselectedColor, "fg_unselected_color", "ScrollUnselectedColor");
|
|
addColorXML(node, mFgDisabledColor, "fg_disable_color", "ScrollDisabledColor");
|
|
addColorXML(node, mHighlightedColor, "highlighted_color", "ScrollHighlightedColor");
|
|
|
|
// Contents
|
|
std::vector<const LLScrollListColumn*> sorted_list;
|
|
sorted_list.resize(mColumns.size());
|
|
for (column_map_t::const_iterator itor = mColumns.begin(); itor != mColumns.end(); ++itor)
|
|
{
|
|
sorted_list[itor->second->mIndex] = itor->second;
|
|
}
|
|
|
|
for (std::vector<const LLScrollListColumn*>::iterator itor2 = sorted_list.begin(); itor2 != sorted_list.end(); ++itor2)
|
|
{
|
|
LLXMLNodePtr child_node = node->createChild("column", FALSE);
|
|
const LLScrollListColumn *column = *itor2;
|
|
|
|
child_node->createChild("name", TRUE)->setStringValue(column->mName);
|
|
child_node->createChild("label", TRUE)->setStringValue(column->mLabel);
|
|
child_node->createChild("width", TRUE)->setIntValue(column->getWidth());
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
void LLScrollListCtrl::setScrollListParameters(LLXMLNodePtr node)
|
|
{
|
|
// James: This is not a good way to do colors. We need a central "UI style"
|
|
// manager that sets the colors for ALL scroll lists, buttons, etc.
|
|
|
|
LLColor4 color;
|
|
if(node->hasAttribute("fg_unselected_color"))
|
|
{
|
|
LLUICtrlFactory::getAttributeColor(node,"fg_unselected_color", color);
|
|
setFgUnselectedColor(color);
|
|
}
|
|
if(node->hasAttribute("fg_selected_color"))
|
|
{
|
|
LLUICtrlFactory::getAttributeColor(node,"fg_selected_color", color);
|
|
setFgSelectedColor(color);
|
|
}
|
|
if(node->hasAttribute("bg_selected_color"))
|
|
{
|
|
LLUICtrlFactory::getAttributeColor(node,"bg_selected_color", color);
|
|
setBgSelectedColor(color);
|
|
}
|
|
if(node->hasAttribute("fg_disable_color"))
|
|
{
|
|
LLUICtrlFactory::getAttributeColor(node,"fg_disable_color", color);
|
|
setFgDisableColor(color);
|
|
}
|
|
if(node->hasAttribute("bg_writeable_color"))
|
|
{
|
|
LLUICtrlFactory::getAttributeColor(node,"bg_writeable_color", color);
|
|
setBgWriteableColor(color);
|
|
}
|
|
if(node->hasAttribute("bg_read_only_color"))
|
|
{
|
|
LLUICtrlFactory::getAttributeColor(node,"bg_read_only_color", color);
|
|
setReadOnlyBgColor(color);
|
|
}
|
|
if (LLUICtrlFactory::getAttributeColor(node,"bg_stripe_color", color))
|
|
{
|
|
setBgStripeColor(color);
|
|
}
|
|
if (LLUICtrlFactory::getAttributeColor(node,"highlighted_color", color))
|
|
{
|
|
setHighlightedColor(color);
|
|
}
|
|
|
|
if(node->hasAttribute("background_visible"))
|
|
{
|
|
BOOL background_visible;
|
|
node->getAttributeBOOL("background_visible", background_visible);
|
|
setBackgroundVisible(background_visible);
|
|
}
|
|
|
|
if(node->hasAttribute("draw_stripes"))
|
|
{
|
|
BOOL draw_stripes;
|
|
node->getAttributeBOOL("draw_stripes", draw_stripes);
|
|
setDrawStripes(draw_stripes);
|
|
}
|
|
|
|
if(node->hasAttribute("column_padding"))
|
|
{
|
|
S32 column_padding;
|
|
node->getAttributeS32("column_padding", column_padding);
|
|
setColumnPadding(column_padding);
|
|
}
|
|
|
|
if (node->hasAttribute("mouse_wheel_opaque"))
|
|
{
|
|
node->getAttribute_bool("mouse_wheel_opaque", mMouseWheelOpaque);
|
|
}
|
|
|
|
if (node->hasAttribute("menu_num"))
|
|
{
|
|
// Some scroll lists use common menus identified by number
|
|
// 0 is menu_avs_list.xml, 1 will be for groups, 2 could be for lists of objects
|
|
S32 menu_num;
|
|
node->getAttributeS32("menu_num", menu_num);
|
|
setContextMenu(menu_num);
|
|
}
|
|
else if (node->hasAttribute("menu_file"))
|
|
{
|
|
std::string menu_file;
|
|
node->getAttributeString("menu_file", menu_file);
|
|
mPopupMenu = LLUICtrlFactory::getInstance()->buildMenu(menu_file, LLMenuGL::sMenuContainer);
|
|
}
|
|
}
|
|
|
|
// static
|
|
LLView* LLScrollListCtrl::fromXML(LLXMLNodePtr node, LLView *parent, LLUICtrlFactory *factory)
|
|
{
|
|
LLRect rect;
|
|
createRect(node, rect, parent, LLRect());
|
|
|
|
BOOL multi_select = false;
|
|
node->getAttributeBOOL("multi_select", multi_select);
|
|
BOOL draw_border = true;
|
|
node->getAttributeBOOL("draw_border", draw_border);
|
|
BOOL draw_heading = false;
|
|
node->getAttributeBOOL("draw_heading", draw_heading);
|
|
S32 search_column = 0;
|
|
node->getAttributeS32("search_column", search_column);
|
|
S32 sort_column = -1;
|
|
node->getAttributeS32("sort_column", sort_column);
|
|
BOOL sort_ascending = true;
|
|
node->getAttributeBOOL("sort_ascending", sort_ascending);
|
|
|
|
LLScrollListCtrl* scroll_list = new LLScrollListCtrl("scroll_list", rect, NULL, multi_select, draw_border, draw_heading);
|
|
|
|
if (node->hasAttribute("heading_height"))
|
|
{
|
|
S32 heading_height;
|
|
node->getAttributeS32("heading_height", heading_height);
|
|
scroll_list->setHeadingHeight(heading_height);
|
|
}
|
|
|
|
scroll_list->setScrollListParameters(node);
|
|
scroll_list->initFromXML(node, parent);
|
|
scroll_list->setSearchColumn(search_column);
|
|
|
|
LLSD columns;
|
|
S32 index = 0;
|
|
const std::string nodename(std::string(node->getName()->mString) + '.');
|
|
const std::string kidcolumn(nodename + "columns");
|
|
for (LLXMLNodePtr child = node->getFirstChild(); child.notNull(); child = child->getNextSibling())
|
|
{
|
|
if (child->hasName("column") || child->hasName(kidcolumn))
|
|
{
|
|
std::string labelname("");
|
|
if (child->getAttributeString("label", labelname))
|
|
columns[index]["label"] = labelname;
|
|
else if (child->getAttributeString("image", labelname))
|
|
columns[index]["image"] = labelname;
|
|
else if (child->getAttributeString("image_overlay", labelname))
|
|
columns[index]["image_overlay"] = labelname;
|
|
|
|
std::string columnname(labelname);
|
|
if (child->getAttributeString("name", columnname))
|
|
columns[index]["name"] = columnname;
|
|
|
|
std::string sortname(columnname);
|
|
if (child->getAttributeString("sort", sortname))
|
|
columns[index]["sort"] = sortname;
|
|
|
|
std::string sort_direction("ascending");
|
|
if (child->getAttributeString("sort_direction", sort_direction))
|
|
{
|
|
columns[index]["sort_direction"] = sort_direction;
|
|
}
|
|
else // Singu Note: if a scroll list does not provide sort_direction, provide sort_ascending to sort as expected
|
|
{
|
|
bool sort_ascending = true;
|
|
child->getAttribute_bool("sort_ascending", sort_ascending);
|
|
columns[index]["sort_ascending"] = sort_ascending;
|
|
}
|
|
|
|
S32 columnwidth = -1;
|
|
if (child->getAttributeS32("width", columnwidth))
|
|
columns[index]["width"] = columnwidth;
|
|
|
|
F32 columnrelwidth = 0.f;
|
|
if (child->getAttributeF32("relative_width", columnrelwidth)
|
|
|| child->getAttributeF32("relwidth", columnrelwidth))
|
|
columns[index]["relative_width"] = columnrelwidth;
|
|
|
|
BOOL columndynamicwidth = false;
|
|
if (child->getAttributeBOOL("dynamic_width", columndynamicwidth)
|
|
|| child->getAttributeBOOL("dynamicwidth", columndynamicwidth)) // Singu TODO: Deprecate "dynamicwidth"
|
|
columns[index]["dynamic_width"] = columndynamicwidth;
|
|
|
|
LLFontGL::HAlign h_align = LLView::selectFontHAlign(child);
|
|
columns[index]["halign"] = (S32)h_align;
|
|
|
|
std::string tooltip;
|
|
if (child->getAttributeString("tool_tip", tooltip))
|
|
columns[index]["tool_tip"] = tooltip;
|
|
++index;
|
|
}
|
|
}
|
|
scroll_list->setColumnHeadings(columns);
|
|
|
|
if (sort_column >= 0)
|
|
{
|
|
scroll_list->sortByColumnIndex(sort_column, sort_ascending);
|
|
}
|
|
|
|
const std::string kidrow(nodename + "row");
|
|
const std::string kidrows(nodename + "rows");
|
|
for (LLXMLNodePtr child = node->getFirstChild(); child.notNull(); child = child->getNextSibling())
|
|
{
|
|
if (child->hasName("row") || child->hasName(kidrow) || child->hasName("rows") || child->hasName(kidrows))
|
|
{
|
|
LLUUID id;
|
|
LLSD row;
|
|
std::string value;
|
|
child->getAttributeString("value",value);
|
|
bool id_found = child->getAttributeUUID("id", id);
|
|
if(id_found)
|
|
row["id"] = id;
|
|
else
|
|
row["id"] = value;
|
|
|
|
S32 column_idx = 0;
|
|
bool explicit_column = false;
|
|
for (LLXMLNodePtr row_child = child->getFirstChild(); row_child.notNull(); row_child = row_child->getNextSibling())
|
|
{
|
|
if (row_child->hasName("column"))
|
|
{
|
|
std::string value = row_child->getTextContents();
|
|
row["columns"][column_idx]["value"] = value;
|
|
|
|
std::string columnname;
|
|
if (row_child->getAttributeString("name", columnname))
|
|
row["columns"][column_idx]["column"] = columnname;
|
|
|
|
std::string font;
|
|
if (row_child->getAttributeString("font", font))
|
|
row["columns"][column_idx]["font"] = font;
|
|
|
|
std::string font_style;
|
|
if (row_child->getAttributeString("font-style", font_style))
|
|
row["columns"][column_idx]["font-style"] = font_style;
|
|
|
|
++column_idx;
|
|
explicit_column = true;
|
|
}
|
|
}
|
|
if(explicit_column)
|
|
scroll_list->addElement(row);
|
|
else
|
|
{
|
|
LLSD entry_id;
|
|
if(id_found)
|
|
entry_id = id;
|
|
scroll_list->addSimpleElement(value,ADD_BOTTOM,entry_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
scroll_list->setCommentText(node->getTextContents());
|
|
return scroll_list;
|
|
}
|
|
|
|
// LLEditMenuHandler functions
|
|
|
|
// virtual
|
|
void LLScrollListCtrl::copy()
|
|
{
|
|
std::string buffer;
|
|
for (auto item : getAllSelected())
|
|
{
|
|
buffer += item->getContentsCSV() + '\n';
|
|
}
|
|
gClipboard.copyFromSubstring(utf8str_to_wstring(buffer), 0, buffer.length());
|
|
}
|
|
|
|
// virtual
|
|
BOOL LLScrollListCtrl::canCopy() const
|
|
{
|
|
return (getFirstSelected() != NULL);
|
|
}
|
|
|
|
// virtual
|
|
void LLScrollListCtrl::cut()
|
|
{
|
|
copy();
|
|
doDelete();
|
|
}
|
|
|
|
// virtual
|
|
BOOL LLScrollListCtrl::canCut() const
|
|
{
|
|
return canCopy() && canDoDelete();
|
|
}
|
|
|
|
// virtual
|
|
void LLScrollListCtrl::selectAll()
|
|
{
|
|
// Deselects all other items
|
|
item_list::iterator iter;
|
|
for (iter = mItemList.begin(); iter != mItemList.end(); iter++)
|
|
{
|
|
LLScrollListItem *itemp = *iter;
|
|
if (itemp->getEnabled() && !itemp->getFiltered())
|
|
{
|
|
selectItem(itemp, FALSE);
|
|
}
|
|
}
|
|
|
|
if (mCommitOnSelectionChange)
|
|
{
|
|
commitIfChanged();
|
|
}
|
|
}
|
|
|
|
// virtual
|
|
BOOL LLScrollListCtrl::canSelectAll() const
|
|
{
|
|
return getCanSelect() && mAllowMultipleSelection && !(mMaxSelectable > 0 && mItemList.size() > mMaxSelectable);
|
|
}
|
|
|
|
// virtual
|
|
void LLScrollListCtrl::deselect()
|
|
{
|
|
deselectAllItems();
|
|
}
|
|
|
|
// virtual
|
|
BOOL LLScrollListCtrl::canDeselect() const
|
|
{
|
|
return getCanSelect();
|
|
}
|
|
|
|
void LLScrollListCtrl::addColumn(const LLSD& column, EAddPosition pos)
|
|
{
|
|
LLScrollListColumn::Params p;
|
|
LLParamSDParser parser;
|
|
parser.readSD(column, p);
|
|
addColumn(p, pos);
|
|
}
|
|
|
|
void LLScrollListCtrl::addColumn(const LLScrollListColumn::Params& column_params, EAddPosition pos)
|
|
{
|
|
if (!column_params.validateBlock()) return;
|
|
|
|
std::string name = column_params.name;
|
|
// if no column name provided, just use ordinal as name
|
|
if (name.empty())
|
|
{
|
|
name = llformat("%d", mColumnsIndexed.size());
|
|
}
|
|
|
|
if (mColumns.find(name) == mColumns.end())
|
|
{
|
|
// Add column
|
|
mColumns[name] = new LLScrollListColumn(column_params, this);
|
|
LLScrollListColumn* new_column = mColumns[name];
|
|
new_column->mIndex = mColumns.size()-1;
|
|
|
|
// Add button
|
|
if (new_column->getWidth() > 0 || new_column->mRelWidth > 0 || new_column->mDynamicWidth)
|
|
{
|
|
if (getNumColumns() > 0)
|
|
{
|
|
mTotalColumnPadding += mColumnPadding;
|
|
}
|
|
if (new_column->mRelWidth >= 0)
|
|
{
|
|
new_column->setWidth((S32)ll_round(new_column->mRelWidth*mItemListRect.getWidth()));
|
|
}
|
|
else if(new_column->mDynamicWidth)
|
|
{
|
|
mNumDynamicWidthColumns++;
|
|
new_column->setWidth((mItemListRect.getWidth() - mTotalStaticColumnWidth - mTotalColumnPadding) / mNumDynamicWidthColumns);
|
|
}
|
|
S32 top = mItemListRect.mTop;
|
|
|
|
S32 left = mItemListRect.mLeft;
|
|
for (column_map_t::iterator itor = mColumns.begin();
|
|
itor != mColumns.end();
|
|
++itor)
|
|
{
|
|
if (itor->second->mIndex < new_column->mIndex &&
|
|
itor->second->getWidth() > 0)
|
|
{
|
|
left += itor->second->getWidth() + mColumnPadding;
|
|
}
|
|
}
|
|
|
|
S32 right = left+new_column->getWidth();
|
|
if (new_column->mIndex != (S32)mColumns.size()-1)
|
|
{
|
|
right += mColumnPadding;
|
|
}
|
|
|
|
LLRect temp_rect = LLRect(left,top+mHeadingHeight,right,top);
|
|
|
|
if (column_params.header.image.isProvided())
|
|
{
|
|
new_column->mHeader = new LLScrollColumnHeader("btn_" + name, temp_rect, new_column, column_params.header.image, column_params.header.image);
|
|
new_column->mHeader->setDrawArrow(false);
|
|
}
|
|
else
|
|
{
|
|
new_column->mHeader = new LLScrollColumnHeader("btn_" + name, temp_rect, new_column);
|
|
if (column_params.header.image_overlay.isProvided())
|
|
{
|
|
new_column->mHeader->setImageOverlay(column_params.header.image_overlay);
|
|
new_column->mHeader->setDrawArrow(false);
|
|
}
|
|
else
|
|
{
|
|
new_column->mHeader->setLabel(column_params.header.label());
|
|
}
|
|
}
|
|
|
|
new_column->mHeader->setToolTip(column_params.tool_tip());
|
|
new_column->mHeader->setTabStop(false);
|
|
new_column->mHeader->setVisible(mDisplayColumnHeaders);
|
|
|
|
addChild(new_column->mHeader);
|
|
|
|
sendChildToFront(mScrollbar);
|
|
}
|
|
}
|
|
|
|
dirtyColumns();
|
|
}
|
|
|
|
// static
|
|
void LLScrollListCtrl::onClickColumn(void *userdata)
|
|
{
|
|
LLScrollListColumn *info = (LLScrollListColumn*)userdata;
|
|
if (!info) return;
|
|
|
|
LLScrollListCtrl *parent = info->mParentCtrl;
|
|
if (!parent) return;
|
|
|
|
S32 column_index = info->mIndex;
|
|
|
|
LLScrollListColumn* column = parent->mColumnsIndexed[info->mIndex];
|
|
bool ascending = column->mSortDirection == LLScrollListColumn::ASCENDING;
|
|
if (column->mSortingColumn != column->mName
|
|
&& parent->mColumns.find(column->mSortingColumn) != parent->mColumns.end())
|
|
{
|
|
LLScrollListColumn* info_redir = parent->mColumns[column->mSortingColumn];
|
|
column_index = info_redir->mIndex;
|
|
}
|
|
|
|
// if this column is the primary sort key, reverse the direction
|
|
sort_column_t cur_sort_column;
|
|
if (!parent->mSortColumns.empty() && parent->mSortColumns.back().first == column_index)
|
|
{
|
|
ascending = !parent->mSortColumns.back().second;
|
|
}
|
|
|
|
parent->sortByColumnIndex(column_index, ascending);
|
|
|
|
if (parent->mOnSortChangedCallback)
|
|
{
|
|
parent->mOnSortChangedCallback();
|
|
}
|
|
}
|
|
|
|
std::string LLScrollListCtrl::getSortColumnName()
|
|
{
|
|
LLScrollListColumn* column = mSortColumns.empty() ? NULL : mColumnsIndexed[mSortColumns.back().first];
|
|
|
|
if (column) return column->mName;
|
|
else return "";
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::hasSortOrder() const
|
|
{
|
|
return mSortEnabled && !mSortColumns.empty();
|
|
}
|
|
|
|
void LLScrollListCtrl::clearSortOrder()
|
|
{
|
|
mSortColumns.clear();
|
|
}
|
|
|
|
void LLScrollListCtrl::clearColumns()
|
|
{
|
|
column_map_t::iterator itor;
|
|
for (itor = mColumns.begin(); itor != mColumns.end(); ++itor)
|
|
{
|
|
LLScrollColumnHeader *header = itor->second->mHeader;
|
|
if (header)
|
|
{
|
|
removeChild(header);
|
|
delete header;
|
|
}
|
|
}
|
|
std::for_each(mColumns.begin(), mColumns.end(), DeletePairedPointer());
|
|
mColumns.clear();
|
|
mSortColumns.clear();
|
|
mTotalStaticColumnWidth = 0;
|
|
mTotalColumnPadding = 0;
|
|
}
|
|
|
|
void LLScrollListCtrl::setColumnLabel(const std::string& column, const std::string& label)
|
|
{
|
|
LLScrollListColumn* columnp = getColumn(column);
|
|
if (columnp)
|
|
{
|
|
columnp->mLabel = label;
|
|
if (columnp->mHeader)
|
|
{
|
|
columnp->mHeader->setLabel(label);
|
|
}
|
|
}
|
|
}
|
|
|
|
LLScrollListColumn* LLScrollListCtrl::getColumn(S32 index)
|
|
{
|
|
if (index < 0 || index >= (S32)mColumnsIndexed.size())
|
|
{
|
|
return NULL;
|
|
}
|
|
return mColumnsIndexed[index];
|
|
}
|
|
|
|
LLScrollListColumn* LLScrollListCtrl::getColumn(const std::string& name)
|
|
{
|
|
column_map_t::iterator column_itor = mColumns.find(name);
|
|
if (column_itor != mColumns.end())
|
|
{
|
|
return column_itor->second;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
void LLScrollListCtrl::setColumnHeadings(const LLSD& headings)
|
|
{
|
|
mColumns.clear();
|
|
LLSD::array_const_iterator itor;
|
|
for (itor = headings.beginArray(); itor != headings.endArray(); ++itor)
|
|
{
|
|
addColumn(*itor);
|
|
}
|
|
}
|
|
|
|
/* "id"
|
|
"enabled"
|
|
"columns"
|
|
"column"
|
|
"name"
|
|
"label"
|
|
"width"
|
|
"dynamic_width"
|
|
*/
|
|
LLTrace::BlockTimerStatHandle FTM_ADD_SCROLLLIST_ELEMENT("Add Scroll List Item");
|
|
LLScrollListItem* LLScrollListCtrl::addElement(const LLSD& element, EAddPosition pos, void* userdata)
|
|
{
|
|
LL_RECORD_BLOCK_TIME(FTM_ADD_SCROLLLIST_ELEMENT);
|
|
LLScrollListItem::Params item_params;
|
|
LLParamSDParser parser;
|
|
parser.readSD(element, item_params);
|
|
item_params.userdata = userdata;
|
|
return addRow(item_params, pos);
|
|
}
|
|
|
|
LLScrollListItem* LLScrollListCtrl::addRow(const LLScrollListItem::Params& item_p, EAddPosition pos)
|
|
{
|
|
LL_RECORD_BLOCK_TIME(FTM_ADD_SCROLLLIST_ELEMENT);
|
|
LLScrollListItem *new_item = new LLScrollListItem(item_p);
|
|
return addRow(new_item, item_p, pos);
|
|
}
|
|
|
|
LLScrollListItem* LLScrollListCtrl::addRow(LLScrollListItem *new_item, const LLScrollListItem::Params& item_p, EAddPosition pos)
|
|
{
|
|
LL_RECORD_BLOCK_TIME(FTM_ADD_SCROLLLIST_ELEMENT);
|
|
if (!item_p.validateBlock() || !new_item) return NULL;
|
|
new_item->setNumColumns(mColumns.size());
|
|
|
|
// Add any columns we don't already have
|
|
S32 col_index = 0;
|
|
|
|
for(LLInitParam::ParamIterator<LLScrollListCell::Params>::const_iterator itor = item_p.columns.begin();
|
|
itor != item_p.columns.end();
|
|
++itor)
|
|
{
|
|
LLScrollListCell::Params cell_p = *itor;
|
|
std::string column = cell_p.column;
|
|
|
|
// empty columns strings index by ordinal
|
|
if (column.empty())
|
|
{
|
|
column = llformat("%d", col_index);
|
|
}
|
|
|
|
LLScrollListColumn* columnp = getColumn(column);
|
|
|
|
// create new column on demand
|
|
if (!columnp)
|
|
{
|
|
LLScrollListColumn::Params new_column;
|
|
new_column.name = column;
|
|
new_column.header.label = column;
|
|
|
|
// if width supplied for column, use it, otherwise
|
|
// use adaptive width
|
|
if (cell_p.width.isProvided())
|
|
{
|
|
new_column.width.pixel_width = cell_p.width;
|
|
}
|
|
addColumn(new_column);
|
|
columnp = mColumns[column];
|
|
new_item->setNumColumns(mColumns.size());
|
|
}
|
|
|
|
S32 index = columnp->mIndex;
|
|
if (!cell_p.width.isProvided())
|
|
{
|
|
cell_p.width = columnp->getWidth();
|
|
}
|
|
cell_p.font_halign = columnp->mFontAlignment;
|
|
|
|
LLScrollListCell* cell = LLScrollListCell::create(cell_p);
|
|
|
|
if (cell)
|
|
{
|
|
new_item->setColumn(index, cell);
|
|
if (columnp->mHeader
|
|
&& cell->isText()
|
|
&& !cell->getValue().asString().empty())
|
|
{
|
|
columnp->mHeader->setHasResizableElement(TRUE);
|
|
}
|
|
}
|
|
|
|
col_index++;
|
|
}
|
|
|
|
if (item_p.columns.empty())
|
|
{
|
|
if (mColumns.empty())
|
|
{
|
|
LLScrollListColumn::Params new_column;
|
|
new_column.name = "0";
|
|
|
|
addColumn(new_column);
|
|
new_item->setNumColumns(mColumns.size());
|
|
}
|
|
|
|
LLScrollListCell* cell = LLScrollListCell::create(LLScrollListCell::Params().value(item_p.value));
|
|
if (cell)
|
|
{
|
|
LLScrollListColumn* columnp = mColumns.begin()->second;
|
|
|
|
new_item->setColumn(0, cell);
|
|
if (columnp->mHeader
|
|
&& cell->isText()
|
|
&& !cell->getValue().asString().empty())
|
|
{
|
|
columnp->mHeader->setHasResizableElement(TRUE);
|
|
}
|
|
}
|
|
}
|
|
|
|
// add dummy cells for missing columns
|
|
for (column_map_t::iterator column_it = mColumns.begin(); column_it != mColumns.end(); ++column_it)
|
|
{
|
|
S32 column_idx = column_it->second->mIndex;
|
|
if (new_item->getColumn(column_idx) == NULL)
|
|
{
|
|
LLScrollListColumn* column_ptr = column_it->second;
|
|
LLScrollListCell::Params cell_p;
|
|
cell_p.width = column_ptr->getWidth();
|
|
|
|
new_item->setColumn(column_idx, new LLScrollListSpacer(cell_p));
|
|
}
|
|
}
|
|
|
|
addItem(new_item, pos);
|
|
return new_item;
|
|
}
|
|
|
|
LLScrollListItem* LLScrollListCtrl::addSimpleElement(const std::string& value, EAddPosition pos, const LLSD& id)
|
|
{
|
|
LLSD entry_id = id;
|
|
|
|
if (id.isUndefined())
|
|
{
|
|
entry_id = value;
|
|
}
|
|
|
|
LLScrollListItem::Params item_params;
|
|
item_params.value(entry_id);
|
|
item_params.columns.add()
|
|
.value(value)
|
|
/*.font(LLFontGL::getFontSansSerifSmall())*/;
|
|
|
|
return addRow(item_params, pos);
|
|
}
|
|
|
|
void LLScrollListCtrl::setValue(const LLSD& value )
|
|
{
|
|
LLSD::array_const_iterator itor;
|
|
for (itor = value.beginArray(); itor != value.endArray(); ++itor)
|
|
{
|
|
addElement(*itor);
|
|
}
|
|
}
|
|
|
|
LLSD LLScrollListCtrl::getValue() const
|
|
{
|
|
LLScrollListItem *item = getFirstSelected();
|
|
if (!item) return LLSD();
|
|
return item->getValue();
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::operateOnSelection(EOperation op)
|
|
{
|
|
if (op == OP_DELETE)
|
|
{
|
|
deleteSelectedItems();
|
|
return TRUE;
|
|
}
|
|
else if (op == OP_DESELECT)
|
|
{
|
|
deselectAllItems();
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
BOOL LLScrollListCtrl::operateOnAll(EOperation op)
|
|
{
|
|
if (op == OP_DELETE)
|
|
{
|
|
clearRows();
|
|
return TRUE;
|
|
}
|
|
else if (op == OP_DESELECT)
|
|
{
|
|
deselectAllItems();
|
|
}
|
|
else if (op == OP_SELECT)
|
|
{
|
|
selectAll();
|
|
}
|
|
return FALSE;
|
|
}
|
|
//virtual
|
|
void LLScrollListCtrl::setFocus(BOOL b)
|
|
{
|
|
// for tabbing into pristine scroll lists (Finder)
|
|
if (!getFirstSelected())
|
|
{
|
|
selectFirstItem();
|
|
//onCommit(); // SJB: selectFirstItem() will call onCommit() if appropriate
|
|
}
|
|
|
|
// Singu Note: Edit menu handler, y'know for Ctrl-A and such!
|
|
if (b) gEditMenuHandler = this;
|
|
else if (gEditMenuHandler == this) gEditMenuHandler = NULL;
|
|
|
|
LLUICtrl::setFocus(b);
|
|
}
|
|
|
|
|
|
// virtual
|
|
BOOL LLScrollListCtrl::isDirty() const
|
|
{
|
|
BOOL grubby = mDirty;
|
|
if ( !mAllowMultipleSelection )
|
|
{
|
|
grubby = (mOriginalSelection != getFirstSelectedIndex());
|
|
}
|
|
return grubby;
|
|
}
|
|
|
|
// Clear dirty state
|
|
void LLScrollListCtrl::resetDirty()
|
|
{
|
|
mDirty = FALSE;
|
|
mOriginalSelection = getFirstSelectedIndex();
|
|
}
|
|
|
|
|
|
//virtual
|
|
void LLScrollListCtrl::onFocusReceived()
|
|
{
|
|
// forget latent selection changes when getting focus
|
|
mSelectionChanged = false;
|
|
LLUICtrl::onFocusReceived();
|
|
}
|
|
|
|
//virtual
|
|
void LLScrollListCtrl::onFocusLost()
|
|
{
|
|
if (hasMouseCapture())
|
|
{
|
|
gFocusMgr.setMouseCapture(NULL);
|
|
}
|
|
|
|
mSearchString.clear();
|
|
|
|
LLUICtrl::onFocusLost();
|
|
}
|
|
|