 /** 
 * @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 "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 <boost/bind.hpp>

#include "fsregistrarutils.h"

static LLDefaultChildRegistry::Register<LLScrollListCtrl> r("scroll_list");

// local structures & classes.
struct SortScrollListItem
{
	SortScrollListItem(const std::vector<std::pair<S32, BOOL> >& 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<std::pair<S32, BOOL> > 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),					// <FS:Beq> FIRE-30732 deferred sort as a UI property
	can_sort("can_sort", true),
	persist_sort_order("persist_sort_order", false),	// <FS:Ansariel> Persists sort order of scroll lists
	primary_sort_only("primary_sort_only", false),		// <FS:Ansariel> 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),
	mPopupMenu(NULL),
	mCommentTextView(NULL),
	mNumDynamicWidthColumns(0),
	mTotalStaticColumnWidth(0),
	mTotalColumnPadding(0),
	mSorted(false),
	mSortLazily(p.sort_lazily),		// <FS:Beq> 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),
	// <FS:Ansariel> 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<S32> 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<LLScrollbar> (sbparams);
	addChild(mScrollbar);

	// Border
	if (p.has_border)
	{
		LLRect border_rect = getLocalRect();
		LLViewBorder::Params params = p.border;
		params.rect(border_rect);
		mBorder = LLUICtrlFactory::create<LLViewBorder> (params);
		addChild(mBorder);
	}

	// set border *after* rect is fully initialized
	if (mBorder)
	{
		mBorder->setRect(getLocalRect());
		mBorder->reshape(getRect().getWidth(), getRect().getHeight());
	}

	// <FS:Ansariel> 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<LLTextBox>(text_p));
	// </FS:Ansariel>

	// <FS:Ansariel> Can only set sort column after we created the actual columns
	//if (p.sort_column >= 0)
	//{
	//	sortByColumnIndex(p.sort_column, p.sort_ascending);
	//}
	// </FS:Ansariel>
	
	for (LLInitParam::ParamIterator<LLScrollListColumn::Params>::const_iterator row_it = p.contents.columns.begin();
		row_it != p.contents.columns.end();
		++row_it)
	{
		addColumn(*row_it);

		// <FS:Ansariel> Get list of the column init params so we can re-add them
		mColumnInitParams.push_back(*row_it);
	}

	// <FS:Ansariel> Can only set sort column after we created the actual columns
	if (p.sort_column >= 0)
	{
		sortByColumnIndex(p.sort_column, p.sort_ascending);
	}
	// </FS:Ansariel>

	for (LLInitParam::ParamIterator<LLScrollListItem::Params>::const_iterator row_it = p.contents.rows.begin();
		row_it != p.contents.rows.end();
		++row_it)
	{
		addRow(*row_it);
	}

	// <FS:Ansariel> 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<LLTextBox>(text_p));
	// </FS:Ansariel>
}

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()
{
	// <FS:Ansariel> Persists sort order of scroll lists
	if (mPersistSortOrder && !mPersistedSortOrderControl.empty())
	{
		LLSD sort_order;
		for (std::vector<sort_column_t>::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);
	}
	// </FS:Ansariel>

	delete mSortCallback;

	std::for_each(mItemList.begin(), mItemList.end(), DeletePointer());
	mItemList.clear();
    clearColumns(); //clears columns and deletes headers
	delete mIsFriendSignal;
}


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
{
	// <FS:Ansariel> 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;
	}
	// </FS:Ansariel> 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<LLScrollListItem*> LLScrollListCtrl::getAllSelected() const
{
	std::vector<LLScrollListItem*> 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;
		// <FS:Ansariel> Fix for FS-specific people list (radar)
		if (isFiltered(item))
		{
			continue;
		}
		// </FS:Ansariel> 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<LLScrollListItem*> LLScrollListCtrl::getAllData() const
{
	std::vector<LLScrollListItem*> 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<S32> 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<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)
	{
		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
	// <FS:KC> 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 )
{
	// <FS:Ansariel> 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 ;
		}
		
		// <FS:Ansariel> Fix for FS-specific people list (radar)
		if (isFiltered(itemp))
		{
			iter++ ;
			continue;
		}
		// </FS:Ansariel> 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;
		// <FS:Ansariel> Fix for FS-specific people list (radar)
		if (isFiltered(itemp))
		{
			continue;
		}
		// </FS:Ansariel> 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<LLTextBox>("comment_text")->setValue(comment_text);
}

// <FS:Ansariel> Allow appending of comment text
void LLScrollListCtrl::addCommentText(const std::string& comment_text)
{
	LLTextBox *ctrl = getChild<LLTextBox>("comment_text");
	ctrl->appendText(comment_text, !ctrl->getText().empty()); // don't prepend newline if empty (Sei)
}
// </FS:Ansariel> 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)
{
	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)
// <FS:Ansariel> Allow selection by substring match
{
	return selectItemByStringMatch(target, true, case_sensitive);
}

BOOL LLScrollListCtrl::selectItemByStringMatch(const LLWString& target, bool prefix_match, BOOL case_sensitive)
// </FS:Ansariel>
{
	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(getSearchColumn());
			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(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);
			
			// <FS:Ansariel> 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;
			}
			// </FS:Ansariel>

			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;
}

// <FS:Ansariel> 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);
}
// </FS:Ansariel>

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
	// <FS:KC> 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<F32> 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);

		// <FS:Ansariel> 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;
		// </FS:Ansariel> Fix for FS-specific people list (radar)

		if (first_line >= mItemList.size())
		{
			return;
		}
		item_list::iterator iter;
		// <FS:Ansariel> 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];
			
			// <FS:Ansariel> Fix for FS-specific people list (radar)
			if (isFiltered(item))
			{
				continue;
			}
		// </FS:Ansariel> 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;
			}
			// <FS:Ansariel> Fix for FS-specific people list (radar)
			line++;
			// </FS:Ansariel> Fix for FS-specific people list (radar)
		}
	}
}


void LLScrollListCtrl::draw()
{
	LLLocalClipRect clip(getLocalRect());

	// <FS:Ansariel> 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;
	}
	// </FS:Ansariel>

	// 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)
{
	// <FS> FIRE-10172: Let the LLTextbox handle the mouse scroll if it's visible
	if (mCommentTextView && mCommentTextView->getVisible())
	{
		return mCommentTextView->handleScrollWheel(x, y, clicks);
	}
	// </FS>

	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 
			// <FS:Zi> 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;
						// <FS:Ansariel> Fix for FS-specific people list (radar)
						if (isFiltered(item))
						{
							continue;
						}
						// </FS:Ansariel> 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.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));

			// <FS:Ansariel> 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));
			// </FS:Ansariel> Additional convenience options

			// <FS:Ansariel> 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));
			// </FS:Ansariel>

			// <FS:Zi> 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));
			// </FS:Zi>

			// 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";
			delete mPopupMenu;
			llassert(LLMenuGL::sMenuContainer != NULL);
			mPopupMenu = LLUICtrlFactory::getInstance()->createFromFile<LLContextMenu>(
				menu_name, LLMenuGL::sMenuContainer, LLMenuHolderGL::child_registry_t::instance());
			if (mPopupMenu)
			{
				if (mIsFriendSignal)
				{
					bool isFriend = *(*mIsFriendSignal)(uuid);
					LLView* addFriendButton = mPopupMenu->getChild<LLView>("add_friend");
					LLView* removeFriendButton = mPopupMenu->getChild<LLView>("remove_friend");

					if (addFriendButton && removeFriendButton)
					{
						addFriendButton->setEnabled(!isFriend);
						removeFriendButton->setEnabled(isFriend);
					}
				}

				mPopupMenu->show(x, y);
				LLMenuGL::showPopup(this, mPopupMenu, 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::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;
				// <FS:ND> Some nullptr handling ...
				//if( item->getSelected() )
				if( item && item->getSelected() && item->getColumn( column_index ) )
				// </FS:ND>
				{
					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
	// <FS:KC> 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;
		// <FS:Ansariel> Fix for FS-specific people list (radar)
		if (isFiltered(item))
		{
			continue;
		}
		// </FS:Ansariel> 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;
			// <FS:Ansariel> 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;
				}
			// </FS:Ansariel>
			default:
				break;
			}
		}
		// TODO: multiple: shift-up, shift-down, shift-home, shift-end, select all
		// <FS:Ansariel> 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;
			}
		}
		// </FS:Ansariel>
	}

	return handled;
}

// <FS:Ansariel> Needed for keyboard selection in radar
void LLScrollListCtrl::setLastSelectedItem(const LLUUID& id)
{
	for (auto item : getAllSelected())
	{
		if (item->getUUID() == id)
		{
			mLastSelected = item;
			break;
		}
	}
}
// </FS:Ansariel>

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<F32> 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<S32, BOOL> 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();

	// <FS:Ansariel> Option to only sort by one column
	if (mPrimarySortOnly)
	{
		clearSortOrder();
	}
	// </FS:Ansariel>

	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
{
	// <FS:Beq> FIRE-30667 et al. Group hang issues
	// if (hasSortOrder() && !isSorted())
	// {
	static LLUICachedControl<U32> 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;
	// </FS:Beq>
		// 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<std::pair<S32, BOOL> > 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<LLScrollListItem*> items = getAllSelected();
	std::vector<LLScrollListItem*>::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<LLScrollColumnHeader>());
			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<LLScrollColumnHeader>(params); 
			addChild(new_column->mHeader);

			sendChildToFront(mScrollbar);
		}
	}

	dirtyColumns();
}

// <FS:Techwolf Lupindo> area search
// area search support for deleting a column
LLScrollListColumn::Params LLScrollListCtrl::delColumn(std::string name)
{
	std::vector<LLScrollListColumn::Params> 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<LLScrollListColumn::Params>::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;
}
// </FS:Techwolf Lupindo> 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;

	// <FS:Ansariel> 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;
}

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();
		}

		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()) // <FS:LO> 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);
}

// <FS:Ansariel> 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;
}
// </FS:Ansariel> Fix for FS-specific people list (radar)

// <FS:Ansariel> Persists sort order of scroll lists
void LLScrollListCtrl::loadPersistedSortOrder()
{
	LLFloater* root_floater = getParentByType<LLFloater>();
	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);
			}
		}
	}
}
// </FS:Ansariel>
