/** * @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 #include "llstl.h" #include "llboost.h" //#include "indra_constants.h" #include "llavatarnamecache.h" #include "llcheckboxctrl.h" #include "llclipboard.h" #include "llfocusmgr.h" #include "llgl.h" // LLGLSUIDefault() #include "lllocalcliprect.h" //#include "llrender.h" #include "llresmgr.h" #include "llscrollbar.h" #include "llscrolllistcell.h" #include "llstring.h" #include "llui.h" #include "lluictrlfactory.h" #include "llwindow.h" #include "llcontrol.h" #include "llkeyboard.h" #include "llviewborder.h" #include "lltextbox.h" #include "llsdparam.h" #include "llcachename.h" #include "llmenugl.h" #include "llurlaction.h" #include "lltooltip.h" #include #include "fsregistrarutils.h" static LLDefaultChildRegistry::Register r("scroll_list"); // local structures & classes. struct SortScrollListItem { SortScrollListItem(const std::vector >& sort_orders,const LLScrollListCtrl::sort_signal_t* sort_signal, bool alternate_sort) : mSortOrders(sort_orders) , mSortSignal(sort_signal) , mAltSort(alternate_sort) {} 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 { if (mAltSort && !cell1->getAltValue().asString().empty() && !cell2->getAltValue().asString().empty()) { sort_result = order * LLStringUtil::compareDict(cell1->getAltValue().asString(), cell2->getAltValue().asString()); } 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 > sort_order_t; const LLScrollListCtrl::sort_signal_t* mSortSignal; const sort_order_t& mSortOrders; const bool mAltSort; }; //--------------------------------------------------------------------------- // LLScrollListCtrl //--------------------------------------------------------------------------- void LLScrollListCtrl::SelectionTypeNames::declareValues() { declare("row", LLScrollListCtrl::ROW); declare("cell", LLScrollListCtrl::CELL); declare("header", LLScrollListCtrl::HEADER); } LLScrollListCtrl::Contents::Contents() : columns("column"), rows("row") { addSynonym(columns, "columns"); addSynonym(rows, "rows"); } LLScrollListCtrl::Params::Params() : multi_select("multi_select", false), has_border("draw_border"), draw_heading("draw_heading"), search_column("search_column", 0), selection_type("selection_type", ROW), sort_column("sort_column", -1), sort_ascending("sort_ascending", true), sort_lazily("sort_lazily", false), // FIRE-30732 deferred sort as a UI property can_sort("can_sort", true), persist_sort_order("persist_sort_order", false), // Persists sort order of scroll lists primary_sort_only("primary_sort_only", false), // Option to only sort by one column mouse_wheel_opaque("mouse_wheel_opaque", false), commit_on_keyboard_movement("commit_on_keyboard_movement", true), commit_on_selection_change("commit_on_selection_change", false), heading_height("heading_height"), page_lines("page_lines", 0), background_visible("background_visible"), draw_stripes("draw_stripes"), column_padding("column_padding"), row_padding("row_padding", 2), fg_unselected_color("fg_unselected_color"), fg_selected_color("fg_selected_color"), bg_selected_color("bg_selected_color"), fg_disable_color("fg_disable_color"), bg_writeable_color("bg_writeable_color"), bg_readonly_color("bg_readonly_color"), bg_stripe_color("bg_stripe_color"), hovered_color("hovered_color"), highlighted_color("highlighted_color"), contents(""), scroll_bar_bg_visible("scroll_bar_bg_visible"), scroll_bar_bg_color("scroll_bar_bg_color"), border("border") {} LLScrollListCtrl::LLScrollListCtrl(const LLScrollListCtrl::Params& p) : LLUICtrl(p), mLineHeight(0), mScrollLines(0), mMouseWheelOpaque(p.mouse_wheel_opaque), mPageLines(p.page_lines), mMaxSelectable(0), mAllowKeyboardMovement(true), mCommitOnKeyboardMovement(p.commit_on_keyboard_movement), mCommitOnSelectionChange(p.commit_on_selection_change), mSelectionChanged(false), mSelectionType(p.selection_type), mNeedsScroll(false), mCanSelect(true), mCanSort(p.can_sort), mColumnsDirty(false), mMaxItemCount(INT_MAX), mBorderThickness( 2 ), mOnDoubleClickCallback( NULL ), mOnMaximumSelectCallback( NULL ), mOnSortChangedCallback( NULL ), mHighlightedItem(-1), mBorder(NULL), mSortCallback(NULL), mCommentTextView(NULL), mNumDynamicWidthColumns(0), mTotalStaticColumnWidth(0), mTotalColumnPadding(0), mSorted(false), mSortLazily(p.sort_lazily), // FIRE-30732 deferred sort configurability mDirty(false), mOriginalSelection(-1), mLastSelected(NULL), mHeadingHeight(p.heading_height), mAllowMultipleSelection(p.multi_select), mDisplayColumnHeaders(p.draw_heading), mBackgroundVisible(p.background_visible), mDrawStripes(p.draw_stripes), mBgWriteableColor(p.bg_writeable_color()), mBgReadOnlyColor(p.bg_readonly_color()), mBgSelectedColor(p.bg_selected_color()), mBgStripeColor(p.bg_stripe_color()), mFgSelectedColor(p.fg_selected_color()), mFgUnselectedColor(p.fg_unselected_color()), mFgDisabledColor(p.fg_disable_color()), mHighlightedColor(p.highlighted_color()), mHoveredColor(p.hovered_color()), mSearchColumn(p.search_column), mColumnPadding(p.column_padding), mRowPadding(p.row_padding), mAlternateSort(false), mContextMenuType(MENU_NONE), mIsFriendSignal(NULL), // Fix for FS-specific people list (radar) mFilterColumn(-1), mIsFiltered(false), mPersistSortOrder(p.persist_sort_order), mPersistedSortOrderLoaded(false), mPersistedSortOrderControl(""), mPrimarySortOnly(p.primary_sort_only) { mItemListRect.setOriginAndSize( mBorderThickness, mBorderThickness, getRect().getWidth() - 2 * mBorderThickness, getRect().getHeight() - 2 * mBorderThickness ); updateLineHeight(); // Init the scrollbar static LLUICachedControl scrollbar_size ("UIScrollbarSize", 0); LLRect scroll_rect; scroll_rect.setOriginAndSize( getRect().getWidth() - mBorderThickness - scrollbar_size, mItemListRect.mBottom, scrollbar_size, mItemListRect.getHeight()); LLScrollbar::Params sbparams; sbparams.name("Scrollbar"); sbparams.rect(scroll_rect); sbparams.orientation(LLScrollbar::VERTICAL); sbparams.doc_size(getItemCount()); sbparams.doc_pos(mScrollLines); sbparams.page_size( getLinesPerPage() ); sbparams.change_callback(boost::bind(&LLScrollListCtrl::onScrollChange, this, _1, _2)); sbparams.follows.flags(FOLLOWS_RIGHT | FOLLOWS_TOP | FOLLOWS_BOTTOM); sbparams.visible(false); sbparams.bg_visible(p.scroll_bar_bg_visible); sbparams.bg_color(p.scroll_bar_bg_color); mScrollbar = LLUICtrlFactory::create (sbparams); addChild(mScrollbar); // Border if (p.has_border) { LLRect border_rect = getLocalRect(); LLViewBorder::Params params = p.border; params.rect(border_rect); mBorder = LLUICtrlFactory::create (params); addChild(mBorder); } // set border *after* rect is fully initialized if (mBorder) { mBorder->setRect(getLocalRect()); mBorder->reshape(getRect().getWidth(), getRect().getHeight()); } // addRow() will call updateLayout() that tries to access the the comment // textbox. So create it first before adding rows! LLTextBox::Params text_p; text_p.name("comment_text"); text_p.border_visible(false); text_p.rect(mItemListRect); text_p.follows.flags(FOLLOWS_ALL); // word wrap was added accroding to the EXT-6841 text_p.wrap(true); // set up label text color for empty lists in a way it's always readable -Zi text_p.text_color = mFgUnselectedColor; // show scroll bar when applicable -Sei text_p.allow_scroll(true); text_p.track_end(true); addChild(LLUICtrlFactory::create(text_p)); // // Can only set sort column after we created the actual columns //if (p.sort_column >= 0) //{ // sortByColumnIndex(p.sort_column, p.sort_ascending); //} // for (LLInitParam::ParamIterator::const_iterator row_it = p.contents.columns.begin(); row_it != p.contents.columns.end(); ++row_it) { addColumn(*row_it); // Get list of the column init params so we can re-add them mColumnInitParams.push_back(*row_it); } // Can only set sort column after we created the actual columns if (p.sort_column >= 0) { sortByColumnIndex(p.sort_column, p.sort_ascending); } // for (LLInitParam::ParamIterator::const_iterator row_it = p.contents.rows.begin(); row_it != p.contents.rows.end(); ++row_it) { addRow(*row_it); } // Moved up //LLTextBox::Params text_p; //text_p.name("comment_text"); //text_p.border_visible(false); //text_p.rect(mItemListRect); //text_p.follows.flags(FOLLOWS_ALL); //// word wrap was added accroding to the EXT-6841 //text_p.wrap(true); //// set up label text color for empty lists in a way it's always readable -Zi //text_p.text_color = mFgUnselectedColor; //addChild(LLUICtrlFactory::create(text_p)); // } 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()); } /*virtual*/ bool LLScrollListCtrl::preProcessChildNode(LLXMLNodePtr child) { if (child->hasName("column") || child->hasName("row")) { return true; // skip } else { return false; } } LLScrollListCtrl::~LLScrollListCtrl() { // Persists sort order of scroll lists if (mPersistSortOrder && !mPersistedSortOrderControl.empty()) { LLSD sort_order; for (std::vector::iterator it = mSortColumns.begin(); it != mSortColumns.end(); ++it) { sort_column_t& col = *it; S32 sort_val = col.first + 1; if (!col.second) { sort_val *= -1; } sort_order.append(LLSD(sort_val)); } LLControlVariable* sort_order_setting = LLUI::getInstance()->mSettingGroups["config"]->declareLLSD(mPersistedSortOrderControl, LLSD(), "Column sort order for control " + mPersistedSortOrderControl); sort_order_setting->setValue(sort_order); } // delete mSortCallback; std::for_each(mItemList.begin(), mItemList.end(), DeletePointer()); mItemList.clear(); clearColumns(); //clears columns and deletes headers delete mIsFriendSignal; auto menu = mPopupMenuHandle.get(); if (menu) { menu->die(); mPopupMenuHandle.markDead(); } } 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 { // Fix for FS-specific people list (radar) if (mIsFiltered) { S32 count(0); item_list::const_iterator iter; for(iter = mItemList.begin(); iter != mItemList.end(); iter++) { LLScrollListItem* item = *iter; std::string filterColumnValue = item->getColumn(mFilterColumn)->getValue().asString(); std::transform(filterColumnValue.begin(), filterColumnValue.end(), filterColumnValue.begin(), ::tolower); if (filterColumnValue.find(mFilterString) == std::string::npos) { continue; } count++; } return count; } // Fix for FS-specific people list (radar) return mItemList.size(); } BOOL LLScrollListCtrl::hasSelectedItem() const { item_list::iterator iter; for (iter = mItemList.begin(); iter < mItemList.end(); ) { LLScrollListItem* itemp = *iter; if (itemp && itemp->getSelected()) { return TRUE; } iter++; } return FALSE; } // 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 { 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 LLScrollListCtrl::getAllSelected() const { std::vector 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; } S32 LLScrollListCtrl::getNumSelected() const { 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 { 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; // Fix for FS-specific people list (radar) if (isFiltered(item)) { continue; } // Fix for FS-specific people list (radar) 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 LLScrollListCtrl::getAllData() const { std::vector ret; item_list::const_iterator iter; for(iter = mItemList.begin(); iter != mItemList.end(); iter++) { LLScrollListItem* item = *iter; ret.push_back(item); } 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() { static LLUICachedControl scrollbar_size ("UIScrollbarSize", 0); // 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) { switch( pos ) { case ADD_TOP: mItemList.push_front(item); setNeedsSort(); break; case ADD_DEFAULT: 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) + mColumnPadding + HEADING_TEXT_PADDING : 0; item_list::iterator iter; for (iter = mItemList.begin(); iter != mItemList.end(); iter++) { LLScrollListCell* cellp = (*iter)->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; } } return width_changed; } // Line height is the max height of all the cells in all the items. void LLScrollListCtrl::updateLineHeight() { mLineHeight = 0; item_list::iterator iter; for (iter = mItemList.begin(); iter != mItemList.end(); iter++) { LLScrollListItem *itemp = *iter; 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() + mRowPadding ); } } } // 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() + mRowPadding ); } } void LLScrollListCtrl::updateColumns(bool force_update) { if (!mColumnsDirty && !force_update) return; mColumnsDirty = false; bool columns_changed_width = updateColumnWidths(); // update column headers std::vector::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) { LLScrollListColumn* column = *column_ordered_it; if (!column || column->getWidth() < 0) { // skip hidden columns continue; } 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; } } bool header_changed_width = false; // expand last column header we encountered to full list width // Fixed last column on LLScrollListCtrl expanding on control resize when column width should be fixed or dynamic //if (last_header) if (last_header && last_header->canResize()) { S32 old_width = last_header->getColumn()->getWidth(); 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); if (old_width != new_width) { last_header->getColumn()->setWidth(new_width); header_changed_width = true; } } // 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()); } } } else if (header_changed_width) { item_list::iterator iter; S32 index = last_header->getColumn()->mIndex; // Not always identical to last column! for (iter = mItemList.begin(); iter != mItemList.end(); iter++) { LLScrollListItem *itemp = *iter; LLScrollListCell* cell = itemp->getColumn(index); if (cell) { cell->setWidth(last_header->getColumn()->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( first_item && itemp->getEnabled() ) { if (!itemp->getSelected()) { switch (mSelectionType) { case CELL: selectItem(itemp, 0); break; case HEADER: case ROW: selectItem(itemp, -1); } } 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 ) { // FIRE-30571: Comboboxes select all items then pressing Page-Up target_index = llclamp(target_index, 0, (S32)mItemList.size() - 1); 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 ; } // Fix for FS-specific people list (radar) if (isFiltered(itemp)) { iter++ ; continue; } // Fix for FS-specific people list (radar) if( index >= first_index && index <= last_index ) { if( itemp->getEnabled() ) { // TODO: support range selection for cells selectItem(itemp, -1, 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 } updateSort(); LLScrollListItem *cur_itemp = mItemList[index]; mItemList[index] = mItemList[index - 1]; mItemList[index - 1] = cur_itemp; } 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::clearHighlightedItems() { for (item_list::iterator iter = mItemList.begin(); iter != mItemList.end(); ++iter) { (*iter)->setHighlighted(false); } } void LLScrollListCtrl::mouseOverHighlightNthItem(S32 target_index) { if (mHighlightedItem != target_index) { if (mHighlightedItem >= 0 && mHighlightedItem < mItemList.size()) { mItemList[mHighlightedItem]->setHoverCell(-1); } 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; uuid_vec_t::iterator iditr; for(iditr = ids.begin(); iditr != ids.end(); ++iditr) { if (item->getEnabled() && (item->getUUID() == (*iditr))) { // TODO: support multiple selection for cells selectItem(item, -1, 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; item_list::const_iterator iter; for (iter = mItemList.begin(); iter != mItemList.end(); iter++) { LLScrollListItem *itemp = *iter; // Fix for FS-specific people list (radar) if (isFiltered(itemp)) { continue; } // Fix for FS-specific people list (radar) if (target_item == itemp) { return index; } index++; } return -1; } S32 LLScrollListCtrl::getItemIndex( const LLUUID& target_id ) const { updateSort(); S32 index = 0; item_list::const_iterator iter; for (iter = mItemList.begin(); iter != mItemList.end(); iter++) { LLScrollListItem *itemp = *iter; 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->getSelected()) { if (prev_item) { selectItem(prev_item, cur_item->getSelectedCell(), !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->getSelected()) { if (next_item) { selectItem(next_item, cur_item->getSelectedCell(), !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("comment_text")->setValue(comment_text); } // Allow appending of comment text void LLScrollListCtrl::addCommentText(const std::string& comment_text) { LLTextBox *ctrl = getChild("comment_text"); ctrl->appendText(comment_text, !ctrl->getText().empty()); // don't prepend newline if empty (Sei) } // Allow appending of 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"; 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, S32 column/* = 0*/) { deselectAllItems(TRUE); // ensure that no stale items are selected, even if we don't find a match LLScrollListItem* item = getItemByLabel(label, case_sensitive, column); bool found = NULL != item; if(found) { selectItem(item, -1); } 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; 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, S32 column) { return selectItemByPrefix(utf8str_to_wstring(target), case_sensitive, column); } // 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, S32 column) // Allow selection by substring match { return selectItemByStringMatch(target, true, case_sensitive); } BOOL LLScrollListCtrl::selectItemByStringMatch(const LLWString& target, bool prefix_match, BOOL case_sensitive, S32 column) // { 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; // Only select enabled items with matching names LLScrollListCell* cellp = item->getColumn(column == -1 ? getSearchColumn() : column); BOOL select = cellp ? item->getEnabled() && ('\0' == cellp->getValue().asString()[0]) : FALSE; if (select) { selectItem(item, -1); 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; // Only select enabled items with matching names LLScrollListCell* cellp = item->getColumn(column == -1 ? getSearchColumn() : column); 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); // Allow selection by substring match //BOOL select = item->getEnabled() && trimmed_label.compare(0, target_trimmed.size(), target_trimmed) == 0; BOOL select; if (prefix_match) { select = item->getEnabled() && trimmed_label.compare(0, target_trimmed.size(), target_trimmed) == 0; } else { select = item->getEnabled() && trimmed_label.find(target_trimmed) != std::string::npos; } // 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, -1); found = TRUE; break; } } } if (mCommitOnSelectionChange) { commitIfChanged(); } return found; } U32 LLScrollListCtrl::searchItems(const std::string& substring, bool case_sensitive, bool focus) { return searchItems(utf8str_to_wstring(substring), case_sensitive, focus); } U32 LLScrollListCtrl::searchItems(const LLWString& substring, bool case_sensitive, bool focus) { U32 found = 0; LLWString substring_trimmed(substring); S32 len = substring_trimmed.size(); if (0 == len) { // at the moment search for empty element is not supported return 0; } else { deselectAllItems(TRUE); if (!case_sensitive) { // do comparisons in lower case LLWStringUtil::toLower(substring_trimmed); } for (item_list::iterator iter = mItemList.begin(); iter != mItemList.end(); iter++) { LLScrollListItem* item = *iter; // Only select enabled items with matching names if (!item->getEnabled()) { continue; } // Fix for FS-specific people list (radar) if (isFiltered(item)) { continue; } // Fix for FS-specific people list (radar) 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 LLWStringUtil::trim(item_label); size_t found_iter = item_label.find(substring_trimmed); if (found_iter != std::string::npos) { // find offset of matching text cellp->highlightText(found_iter, substring_trimmed.size()); selectItem(item, -1, FALSE); found++; if (!mAllowMultipleSelection) { break; } } } } if (focus && found != 0) { mNeedsScroll = true; } if (mCommitOnSelectionChange) { commitIfChanged(); } return found; } // Allow selection by substring match BOOL LLScrollListCtrl::selectItemBySubstring(const std::string& target, BOOL case_sensitive) { return selectItemBySubstring(utf8str_to_wstring(target), case_sensitive); } // Returns false if item not found. BOOL LLScrollListCtrl::selectItemBySubstring(const LLWString& target, BOOL case_sensitive) { return selectItemByStringMatch(target, false, case_sensitive); } // 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->getEnabled()) { if (value.isBinary()) { if (item->getValue().isBinary()) { LLSD::Binary data1 = value.asBinary(); LLSD::Binary data2 = item->getValue().asBinary(); found = std::equal(data1.begin(), data1.end(), data2.begin()) ? TRUE : FALSE; } } else { found = item->getValue().asString() == value.asString() ? TRUE : FALSE; } if (found) { if (selected) { selectItem(item, -1); } else { deselectItem(item); } 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->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 // Show partial bottom lines on LLScrollListCtrl when list is >1 page long //S32 num_page_lines = getLinesPerPage(); S32 num_page_lines = getLinesPerPage() + 1; LLRect item_rect; LLGLSUIDefault gls_ui; F32 alpha = getCurrentTransparency(); // Don't rely on the current getDrawContext().mAlpha value -Zi { LLLocalClipRect clip(mItemListRect); S32 cur_y = y; S32 max_columns = 0; LLColor4 highlight_color = LLColor4::white; // ex: text inside cells static LLUICachedControl type_ahead_timeout ("TypeAheadTimeout", 0); highlight_color.mV[VALPHA] = clamp_rescale(mSearchTimer.getElapsedTimeF32(), type_ahead_timeout * 0.7f, type_ahead_timeout(), 0.4f, 0.f); // Fix for FS-specific people list (radar) //S32 first_line = mScrollLines; //S32 last_line = llmin((S32)mItemList.size() - 1, mScrollLines + getLinesPerPage()); S32 first_line; S32 last_line; if (mIsFiltered) { first_line = 0; last_line = (S32)mItemList.size() - 1; } else { first_line = mScrollLines; last_line = llmin((S32)mItemList.size() - 1, mScrollLines + getLinesPerPage()); } S32 line = first_line; // Fix for FS-specific people list (radar) if (first_line >= mItemList.size()) { return; } item_list::iterator iter; // Fix for FS-specific people list (radar) //for (S32 line = first_line; line <= last_line; line++) //{ // LLScrollListItem* item = mItemList[line]; for (S32 itline = first_line; itline <= last_line; itline++) { LLScrollListItem* item = mItemList[itline]; // Fix for FS-specific people list (radar) if (isFiltered(item)) { continue; } // Fix for FS-specific people list (radar) item_rect.setOriginAndSize( x, cur_y, mItemListRect.getWidth(), mLineHeight ); item->setRect(item_rect); max_columns = llmax(max_columns, item->getNumColumns()); LLColor4 fg_color; LLColor4 hover_color(LLColor4::transparent); LLColor4 select_color(LLColor4::transparent); if( mScrollLines <= line && line < mScrollLines + num_page_lines ) { fg_color = (item->getEnabled() ? mFgUnselectedColor.get() : mFgDisabledColor.get()); if( item->getSelected() && mCanSelect) { if(item->getHighlighted()) // if it's highlighted, average the colors { select_color = lerp(mBgSelectedColor.get(), mHighlightedColor.get(), 0.5f); } else // otherwise just select-highlight it { select_color = mBgSelectedColor.get(); } fg_color = (item->getEnabled() ? mFgSelectedColor.get() : mFgDisabledColor.get()); } if (mHighlightedItem == line && mCanSelect) { if(item->getHighlighted()) // if it's highlighted, average the colors { hover_color = lerp(mHoveredColor.get(), mHighlightedColor.get(), 0.5f); } else // otherwise just hover-highlight it { hover_color = mHoveredColor.get(); } } else if (item->getHighlighted()) { hover_color = mHighlightedColor.get(); } else { // Why no stripes in single columns? This should be decided by the skin. -Zi if (mDrawStripes && (line % 2 == 0)) // && (max_columns > 1)) { hover_color = mBgStripeColor.get(); } } if (!item->getEnabled()) { hover_color = mBgReadOnlyColor.get(); } item->draw(item_rect, fg_color % alpha, hover_color% alpha, select_color% alpha, highlight_color % alpha, mColumnPadding); cur_y -= mLineHeight; } // Fix for FS-specific people list (radar) line++; // Fix for FS-specific people list (radar) } } } void LLScrollListCtrl::draw() { LLLocalClipRect clip(getLocalRect()); // Persists sort order of scroll lists // This is ugly to do it in draw(), but we don't have the parent // floater in the ctor or postBuild yet (we need it to have a unique // control setting name) if (mPersistSortOrder && !mPersistedSortOrderLoaded) { loadPersistedSortOrder(); mPersistedSortOrderLoaded = true; } // // 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) { F32 alpha = getCurrentTransparency(); gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); gl_rect_2d(background, getEnabled() ? mBgWriteableColor.get() % alpha : mBgReadOnlyColor.get() % alpha ); } updateColumns(); if (mCommentTextView) { mCommentTextView->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) { // FIRE-10172: Let the LLTextbox handle the mouse scroll if it's visible if (mCommentTextView && mCommentTextView->getVisible()) { return mCommentTextView->handleScrollWheel(x, y, clicks); } // BOOL handled = FALSE; // Pretend the mouse is over the scrollbar handled = mScrollbar->handleScrollWheel( 0, 0, clicks ); if (mMouseWheelOpaque) { return TRUE; } return handled; } BOOL LLScrollListCtrl::handleScrollHWheel(S32 x, S32 y, S32 clicks) { BOOL handled = FALSE; // Pretend the mouse is over the scrollbar handled = mScrollbar->handleScrollHWheel( 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, MASK mask) { 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 // Why would we restrict tool tips to text fields only? // Having them on non-text fields seems really useful // && hit_cell->isText() && 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); // display tooltip exactly over original cell, in same font LLToolTipMgr::instance().show(LLToolTip::Params() .message(hit_cell->getToolTip()) .font(LLFontGL::getFontSansSerifSmall()) .pos(LLCoordGL(sticky_rect.mLeft - 5, sticky_rect.mTop + 6)) .delay_time(0.2f) .sticky_rect(sticky_rect)); } handled = TRUE; } // otherwise, look for a tooltip associated with this column LLScrollColumnHeader* headerp = columnp->mHeader; if (headerp && !handled) { handled = headerp->handleToolTip(x, y, mask); } 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, getColumnIndexFromOffset(x)); } 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; for (itor = mItemList.begin(); itor != mItemList.end(); ++itor) { if(mMaxSelectable > 0 && getAllSelected().size() >= mMaxSelectable) { if(mOnMaximumSelectCallback) { mOnMaximumSelectCallback(); } break; } LLScrollListItem *item = *itor; // Fix for FS-specific people list (radar) if (isFiltered(item)) { continue; } // Fix for FS-specific people list (radar) if (item == hit_item || item == lastSelected) { selectItem(item, getColumnIndexFromOffset(x), 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, getColumnIndexFromOffset(x), FALSE); } } } } else if (mask & MASK_CONTROL) { if (hit_item->getSelected()) { deselectItem(hit_item); } else { if(!(mMaxSelectable > 0 && getAllSelected().size() >= mMaxSelectable)) { selectItem(hit_item, getColumnIndexFromOffset(x), FALSE); } else { if(mOnMaximumSelectCallback) { mOnMaximumSelectCallback(); } } } } else { deselectAllItems(TRUE); selectItem(hit_item, getColumnIndexFromOffset(x)); } } else { selectItem(hit_item, getColumnIndexFromOffset(x)); } 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) { LLScrollListItem *item = hitItem(x, y); if (item) { // check to see if we have a UUID for this row std::string id = item->getValue().asString(); LLUUID uuid(id); if (! uuid.isNull() && mContextMenuType != MENU_NONE) { // set up the callbacks for all of the avatar/group menu items // (N.B. callbacks don't take const refs as id is local scope) bool is_group = (mContextMenuType == MENU_GROUP); LLUICtrl::CommitCallbackRegistry::ScopedRegistrar registrar; registrar.add("Url.ShowProfile", boost::bind(&LLScrollListCtrl::showProfile, id, is_group)); registrar.add("Url.SendIM", boost::bind(&LLScrollListCtrl::sendIM, id)); registrar.add("Url.AddFriend", boost::bind(&LLScrollListCtrl::addFriend, id)); registrar.add("Url.RemoveFriend", boost::bind(&LLScrollListCtrl::removeFriend, id)); registrar.add("Url.ReportAbuse", boost::bind(&LLScrollListCtrl::reportAbuse, id, is_group)); registrar.add("Url.Execute", boost::bind(&LLScrollListCtrl::showNameDetails, id, is_group)); registrar.add("Url.CopyLabel", boost::bind(&LLScrollListCtrl::copyNameToClipboard, id, is_group)); registrar.add("Url.CopyUrl", boost::bind(&LLScrollListCtrl::copySLURLToClipboard, id, is_group)); // Additional convenience options registrar.add("FS.ZoomIn", boost::bind(&LLUrlAction::executeSLURL, "secondlife:///app/firestorm/" + id + "/zoom", true)); registrar.add("FS.TeleportToTarget", boost::bind(&LLUrlAction::executeSLURL, "secondlife:///app/firestorm/" + id + "/teleportto", true)); registrar.add("FS.OfferTeleport", boost::bind(&LLUrlAction::executeSLURL, "secondlife:///app/firestorm/" + id + "/offerteleport", true)); registrar.add("FS.RequestTeleport", boost::bind(&LLUrlAction::executeSLURL, "secondlife:///app/firestorm/" + id + "/requestteleport", true)); registrar.add("FS.TrackAvatar", boost::bind(&LLUrlAction::executeSLURL, "secondlife:///app/firestorm/" + id + "/track", true)); registrar.add("FS.AddToContactSet", boost::bind(&LLUrlAction::executeSLURL, "secondlife:///app/firestorm/" + id + "/addtocontactset", true)); // [FS:CR] registrar.add("FS.BlockAvatar", boost::bind(&LLUrlAction::executeSLURL, "secondlife:///app/firestorm/" + id + "/blockavatar", true)); registrar.add("FS.ViewLog", boost::bind(&LLUrlAction::executeSLURL, "secondlife:///app/firestorm/" + id + "/viewlog", true)); // Additional convenience options // Add enable checks for menu items LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enable_registrar; enable_registrar.add("Url.EnableShowProfile", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_ACT_SHOW_PROFILE)); enable_registrar.add("Url.EnableAddFriend", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_ACT_ADD_FRIEND)); enable_registrar.add("Url.EnableRemoveFriend", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_ACT_REMOVE_FRIEND)); enable_registrar.add("Url.EnableSendIM", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_ACT_SEND_IM)); enable_registrar.add("FS.EnableZoomIn", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_ACT_ZOOM_IN)); enable_registrar.add("FS.EnableOfferTeleport", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_ACT_OFFER_TELEPORT)); enable_registrar.add("FS.EnableTrackAvatar", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_ACT_TRACK_AVATAR)); enable_registrar.add("FS.EnableTeleportToTarget", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_ACT_TELEPORT_TO)); enable_registrar.add("FS.EnableRequestTeleport", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_ACT_REQUEST_TELEPORT)); enable_registrar.add("FS.CheckIsAgentBlocked", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_CHK_AVATAR_BLOCKED)); enable_registrar.add("FS.EnableBlockAvatar", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_CHK_IS_NOT_SELF)); enable_registrar.add("FS.EnableViewLog", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_ACT_VIEW_TRANSCRIPT)); // // FIRE-30725 - Add more group functions to group URL context menu std::string uuid_str = uuid.asString(); registrar.add("FS.JoinGroup", boost::bind(&LLUrlAction::executeSLURL, "secondlife:///app/firestorm/" + uuid_str + "/groupjoin", true)); registrar.add("FS.LeaveGroup", boost::bind(&LLUrlAction::executeSLURL, "secondlife:///app/firestorm/" + uuid_str + "/groupleave", true)); registrar.add("FS.ActivateGroup", boost::bind(&LLUrlAction::executeSLURL, "secondlife:///app/firestorm/" + uuid_str + "/groupactivate", true)); enable_registrar.add("FS.WaitingForGroupData", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_CHK_WAITING_FOR_GROUP_DATA)); enable_registrar.add("FS.HaveGroupData", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_CHK_HAVE_GROUP_DATA)); enable_registrar.add("FS.EnableJoinGroup", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_CHK_CAN_JOIN_GROUP)); enable_registrar.add("FS.EnableLeaveGroup", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_CHK_CAN_LEAVE_GROUP)); enable_registrar.add("FS.EnableActivateGroup", boost::bind(&FSRegistrarUtils::checkIsEnabled, gFSRegistrarUtils, uuid, FS_RGSTR_CHK_GROUP_NOT_ACTIVE)); // // create the context menu from the XUI file and display it std::string menu_name = is_group ? "menu_url_group.xml" : "menu_url_agent.xml"; auto menu = mPopupMenuHandle.get(); if (menu) { menu->die(); mPopupMenuHandle.markDead(); } llassert(LLMenuGL::sMenuContainer != NULL); menu = LLUICtrlFactory::getInstance()->createFromFile( menu_name, LLMenuGL::sMenuContainer, LLMenuHolderGL::child_registry_t::instance()); if (menu) { mPopupMenuHandle = menu->getHandle(); if (mIsFriendSignal) { bool isFriend = *(*mIsFriendSignal)(uuid); LLView* addFriendButton = menu->getChild("add_friend"); LLView* removeFriendButton = menu->getChild("remove_friend"); if (addFriendButton && removeFriendButton) { addFriendButton->setEnabled(!isFriend); removeFriendButton->setEnabled(isFriend); } } menu->show(x, y); LLMenuGL::showPopup(this, menu, x, y); return TRUE; } } return LLUICtrl::handleRightMouseDown(x, y, mask); } return FALSE; } void LLScrollListCtrl::showProfile(std::string id, bool is_group) { // show the resident's profile or the group profile std::string sltype = is_group ? "group" : "agent"; std::string slurl = "secondlife:///app/" + sltype + "/" + id + "/about"; LLUrlAction::showProfile(slurl); } void LLScrollListCtrl::sendIM(std::string id) { // send im to the resident std::string slurl = "secondlife:///app/agent/" + id + "/about"; LLUrlAction::sendIM(slurl); } void LLScrollListCtrl::addFriend(std::string id) { // add resident to friends list std::string slurl = "secondlife:///app/agent/" + id + "/about"; LLUrlAction::addFriend(slurl); } void LLScrollListCtrl::removeFriend(std::string id) { std::string slurl = "secondlife:///app/agent/" + id + "/about"; LLUrlAction::removeFriend(slurl); } void LLScrollListCtrl::reportAbuse(std::string id, bool is_group) { if (!is_group) { std::string slurl = "secondlife:///app/agent/" + id + "/about"; LLUrlAction::reportAbuse(slurl); } } void LLScrollListCtrl::showNameDetails(std::string id, bool is_group) { // open the resident's details or the group details std::string sltype = is_group ? "group" : "agent"; std::string slurl = "secondlife:///app/" + sltype + "/" + id + "/about"; LLUrlAction::clickAction(slurl, true); } void LLScrollListCtrl::copyNameToClipboard(std::string id, bool is_group) { // copy the name of the avatar or group to the clipboard std::string name; if (is_group) { gCacheName->getGroupName(LLUUID(id), name); } else { LLAvatarName av_name; LLAvatarNameCache::get(LLUUID(id), &av_name); name = av_name.getAccountName(); } LLUrlAction::copyURLToClipboard(name); } void LLScrollListCtrl::copySLURLToClipboard(std::string id, bool is_group) { // copy a SLURL for the avatar or group to the clipboard std::string sltype = is_group ? "group" : "agent"; std::string slurl = "secondlife:///app/" + sltype + "/" + id + "/about"; LLUrlAction::copyURLToClipboard(slurl); } 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; // Some nullptr handling ... //if( item->getSelected() ) if( item && item->getSelected() && item->getColumn( column_index ) ) // { LLScrollListCell* cellp = item->getColumn(column_index); cellp->setValue(item_value); cellp->onCommit(); if (mLastSelected == NULL) { break; } } } //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 // Show partial bottom lines on LLScrollListCtrl when list is >1 page long //S32 num_page_lines = getLinesPerPage(); S32 num_page_lines = getLinesPerPage() + 1; S32 line = 0; item_list::iterator iter; for(iter = mItemList.begin(); iter != mItemList.end(); iter++) { LLScrollListItem* item = *iter; // Fix for FS-specific people list (radar) if (isFiltered(item)) { continue; } // Fix for FS-specific people list (radar) 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; } 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)); switch (mSelectionType) { case CELL: item->setHoverCell(getColumnIndexFromOffset(x)); break; case HEADER: { S32 cell = getColumnIndexFromOffset(x); if (cell > 0) { item->setHoverCell(cell); } else { item->setHoverCell(-1); } break; } case ROW: break; } } 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) { 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_LEFT: if (mAllowKeyboardMovement || hasFocus()) { // TODO: support multi-select LLScrollListItem *item = getFirstSelected(); if (item) { S32 cell = item->getSelectedCell(); switch (mSelectionType) { case CELL: if (cell < mColumns.size()) cell++; break; case HEADER: if (cell == -1) cell = 1; else if (cell > 1 && cell < mColumns.size()) cell++; // skip header break; case ROW: cell = -1; break; } item->setSelectedCell(cell); handled = TRUE; } } break; case KEY_RIGHT: if (mAllowKeyboardMovement || hasFocus()) { // TODO: support multi-select LLScrollListItem *item = getFirstSelected(); if (item) { S32 cell = item->getSelectedCell(); switch (mSelectionType) { case CELL: if (cell >= 0) cell--; break; case HEADER: if (cell > 1) cell--; else if (cell == 1) cell = -1; // skip header break; case ROW: cell = -1; break; } item->setSelectedCell(cell); 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; // FIRE-19933: Open context menu on context menu key press case KEY_CONTEXT_MENU: { LLScrollListItem* selected_item = getFirstSelected(); if (selected_item) { handleRightMouseDown(selected_item->getRect().getCenterX(), selected_item->getRect().getCenterY(), MASK_NONE); handled = TRUE; } break; } // default: break; } } // TODO: multiple: shift-up, shift-down, shift-home, shift-end, select all // Let's just do this! else if (mask == MASK_CONTROL) { switch (key) { case 'A': if (canSelectAll()) { selectAll(); handled = TRUE; } break; default: break; } } else if (mask == MASK_SHIFT) { switch (key) { case KEY_UP: if (mAllowKeyboardMovement || hasFocus()) { auto selected_items = getAllSelected(); auto last = selected_items.back(); if (mLastSelected == last && selected_items.size() > 1) { deselectItem(last); mLastSelected = getAllSelected().back(); // Use updated selection } else { auto items = getAllData(); auto first = std::find(items.begin(), items.end(), selected_items.front()); if (first != items.end() && first > items.begin()) { selectItem(*(--first), FALSE); } } handled = TRUE; } break; case KEY_DOWN: if (mAllowKeyboardMovement || hasFocus()) { auto selected_items = getAllSelected(); auto first = selected_items.front(); if (mLastSelected == first && selected_items.size() > 1) { deselectItem(first); mLastSelected = getAllSelected().front(); // Use updated selection } else { auto items = getAllData(); auto last = std::find(items.begin(), items.end(), selected_items.back()); if (last != items.end() && last < items.end() - 1) { selectItem(*(++last), FALSE); } } handled = TRUE; } break; case KEY_HOME: if (mAllowKeyboardMovement || hasFocus()) { auto items = getAllData(); auto first = std::find(items.begin(), items.end(), getAllSelected().front()); for (auto it = items.begin(); it != items.end(); ++it) { if (it <= first) { selectItem(*it, FALSE); } else { deselectItem(*it); } } handled = TRUE; } break; case KEY_END: if (mAllowKeyboardMovement || hasFocus()) { auto items = getAllData(); auto last = std::find(items.begin(), items.end(), getAllSelected().back()); for (auto it = items.begin(); it != items.end(); ++it) { if (it >= last) { selectItem(*it, FALSE); } else { deselectItem(*it); } } handled = TRUE; } break; default: break; } } // } return handled; } // Needed for keyboard selection in radar void LLScrollListCtrl::setLastSelectedItem(const LLUUID& id) { for (auto item : getAllSelected()) { if (item->getUUID() == id) { mLastSelected = item; break; } } } // 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 static LLUICachedControl type_ahead_timeout ("TypeAheadTimeout", 0); if (mSearchTimer.getElapsedTimeF32() > type_ahead_timeout) { 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; 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, -1); 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, S32 cell, 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); switch (mSelectionType) { case CELL: itemp->setSelectedCell(cell); break; case HEADER: itemp->setSelectedCell(cell <= 0 ? -1 : cell); break; case ROW: itemp->setSelectedCell(-1); break; } 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()(std::pair 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(); // Option to only sort by one column if (mPrimarySortOnly) { clearSortOrder(); } // 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); } } 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 { // FIRE-30667 et al. Group hang issues // if (hasSortOrder() && !isSorted()) // { static LLUICachedControl sortDeferFrameCount("FSSortDeferalFrames"); if ( hasSortOrder() && !isSorted() && ( !mSortLazily || // if deferred sorting is off OR the deferral period has been exceeded ( mLastUpdateFrame > 1 && ( LLFrameTimer::getFrameCount() - mLastUpdateFrame ) >= sortDeferFrameCount ) ) ) // encoding two (unlikely) special values into mLastUpdateFrame 1 means we've sorted and 0 means we've nothing new to do. // 0 is set after sorting, 1 can be set by a parent for any post sorting action. { mLastUpdateFrame=0; // // do stable sort to preserve any previous sorts std::stable_sort( mItemList.begin(), mItemList.end(), SortScrollListItem(mSortColumns,mSortCallback, mAlternateSort)); mSorted = true; } } // for one-shot sorts, does not save sort column/order void LLScrollListCtrl::sortOnce(S32 column, BOOL ascending) { std::vector > 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,mAlternateSort)); } 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()); } // LLEditMenuHandler functions // virtual void LLScrollListCtrl::copy() { std::string buffer; std::vector items = getAllSelected(); std::vector::iterator itor; for (itor = items.begin(); itor != items.end(); ++itor) { buffer += (*itor)->getContentsCSV() + "\n"; } LLClipboard::instance().copyToClipboard(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() ) { selectItem(itemp, -1, 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); LLScrollColumnHeader::Params params(LLUICtrlFactory::getDefaultParams()); params.name = "btn_" + name; params.rect = temp_rect; params.column = new_column; params.tool_tip = column_params.tool_tip; params.tab_stop = false; params.visible = mDisplayColumnHeaders; if(column_params.header.image.isProvided()) { params.image_selected = column_params.header.image; params.image_unselected = column_params.header.image; } else { params.label = column_params.header.label; } new_column->mHeader = LLUICtrlFactory::create(params); addChild(new_column->mHeader); sendChildToFront(mScrollbar); } } dirtyColumns(); } // area search // area search support for deleting a column LLScrollListColumn::Params LLScrollListCtrl::delColumn(std::string name) { std::vector column_params; LLScrollListColumn::Params params; // save params for each column ordered_columns_t::iterator column_itor; for (column_itor = mColumnsIndexed.begin(); column_itor != mColumnsIndexed.end(); ++column_itor) { LLScrollListColumn* column = (*column_itor); params.header.label = column->mLabel; params.name = column->mName; params.width.dynamic_width = column->mDynamicWidth; params.width.relative_width = column->mRelWidth; params.width.pixel_width = column->getWidth(); params.halign = column->mFontAlignment; LLScrollColumnHeader *header = column->mHeader; if (header) { params.tool_tip = header->getToolTip(); } column_params.push_back(params); } clearColumns(); // restore colums except named column. for (std::vector::iterator iter = column_params.begin(); iter != column_params.end(); ++iter) { std::string i_name = iter->name; if (i_name != name) { addColumn((*iter)); } else { params = (*iter); } } return params; } // area search // static void LLScrollListCtrl::onClickColumn(void *userdata) { LLScrollListColumn *info = (LLScrollListColumn*)userdata; if (!info) return; LLScrollListCtrl *parent = info->mParentCtrl; if (!parent) return; if (!parent->mCanSort) 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 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 !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; // Reset number of dynamic columns, too mNumDynamicWidthColumns = 0; dirtyColumns(); // Clears mColumnsIndexed } 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; } LLScrollListItem* LLScrollListCtrl::addElement(const LLSD& element, EAddPosition pos, void* userdata) { LL_PROFILE_ZONE_SCOPED_CATEGORY_UI; 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_PROFILE_ZONE_SCOPED_CATEGORY_UI; 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_PROFILE_ZONE_SCOPED_CATEGORY_UI; 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::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(); } 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()) if (!getFirstSelected() && !getEnabled()) // make disabled lists not jump to the top on clicking an element in them. { selectFirstItem(); //onCommit(); // SJB: selectFirstItem() will call onCommit() if appropriate } 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(); } boost::signals2::connection LLScrollListCtrl::setIsFriendCallback(const is_friend_signal_t::slot_type& cb) { if (!mIsFriendSignal) { mIsFriendSignal = new is_friend_signal_t(); } return mIsFriendSignal->connect(cb); } bool LLScrollListCtrl::highlightMatchingItems(const std::string& filter_str) { if (filter_str == "" || filter_str == " ") { clearHighlightedItems(); return false; } bool res = false; setHighlightedColor(LLUIColorTable::instance().getColor("SearchableControlHighlightColor", LLColor4::red)); std::string filter_str_lc(filter_str); LLStringUtil::toLower(filter_str_lc); std::vector data = getAllData(); std::vector::iterator iter = data.begin(); while (iter != data.end()) { LLScrollListCell* cell = (*iter)->getColumn(0); if (cell) { std::string value = cell->getValue().asString(); LLStringUtil::toLower(value); if (value.find(filter_str_lc) == std::string::npos) { (*iter)->setHighlighted(false); } else { (*iter)->setHighlighted(true); res = true; } } iter++; } return res; } // Fix for FS-specific people list (radar) void LLScrollListCtrl::setFilterString(const std::string& str) { mFilterString = str; std::transform(mFilterString.begin(), mFilterString.end(), mFilterString.begin(), ::tolower); mIsFiltered = (mFilterColumn > -1 && !mFilterString.empty()); updateLayout(); if (mIsFiltered && getNumSelected() > 0 && isFiltered(getFirstSelected())) { for (item_list::iterator iter = mItemList.begin(); iter != mItemList.end(); iter++) { if (!isFiltered(*iter)) { selectItem(*iter, -1); break; } } } } bool LLScrollListCtrl::isFiltered(const LLScrollListItem* item) const { if (mIsFiltered) { std::string filterColumnValue = item->getColumn(mFilterColumn)->getValue().asString(); std::transform(filterColumnValue.begin(), filterColumnValue.end(), filterColumnValue.begin(), ::tolower); if (filterColumnValue.find(mFilterString) == std::string::npos) { return true; } } return false; } // Fix for FS-specific people list (radar) // Persists sort order of scroll lists void LLScrollListCtrl::loadPersistedSortOrder() { LLFloater* root_floater = getParentByType(); if (root_floater) { mPersistedSortOrderControl = root_floater->getName() + "_" + getName() + "_sortorder"; if (LLUI::getInstance()->mSettingGroups["config"]->controlExists(mPersistedSortOrderControl)) { clearSortOrder(); LLSD sort_order = LLUI::getInstance()->mSettingGroups["config"]->getLLSD(mPersistedSortOrderControl); for (LLSD::array_iterator it = sort_order.beginArray(); it != sort_order.endArray(); ++it) { S32 sort_val = (*it).asInteger(); BOOL ascending = sort_val > 0; sort_val = llabs(sort_val) - 1; setSort(sort_val, ascending); } } } } //