summaryrefslogtreecommitdiff
path: root/indra/llui/llflatlistview.cpp
diff options
context:
space:
mode:
authorErik Kundiman <erik@megapahit.org>2026-09-01 18:35:23 +0800
committerErik Kundiman <erik@megapahit.org>2026-09-04 21:58:53 +0800
commit00bb3bb6f07660bd914d29a1389342bb3060d254 (patch)
tree431f574922494d2201718f13085f17bc6bd7fb42 /indra/llui/llflatlistview.cpp
parenta8636720129952c10cf49ab80da21bf8b340b96c (diff)
parent4ef9f8f14b35ed388c294d53767a0dca0e890924 (diff)
Merge remote-tracking branch 'secondlife/release/26.4' into 26.4
Diffstat (limited to 'indra/llui/llflatlistview.cpp')
-rw-r--r--indra/llui/llflatlistview.cpp473
1 files changed, 465 insertions, 8 deletions
diff --git a/indra/llui/llflatlistview.cpp b/indra/llui/llflatlistview.cpp
index 34eb1ea3fc..d863cc5143 100644
--- a/indra/llui/llflatlistview.cpp
+++ b/indra/llui/llflatlistview.cpp
@@ -31,6 +31,14 @@
#include "llflatlistview.h"
+#include "llfocusmgr.h"
+#include "llrender2dutils.h"
+#include "llui.h"
+#include "lluicolortable.h"
+#include "llwindow.h"
+
+#include <algorithm>
+
static const LLDefaultChildRegistry::Register<LLFlatListView> flat_list_view("flat_list_view");
const LLSD SELECTED_EVENT = LLSD().with("selected", true);
@@ -45,17 +53,28 @@ LLFlatListView::Params::Params()
multi_select("multi_select"),
keep_one_selected("keep_one_selected"),
keep_selection_visible_on_reshape("keep_selection_visible_on_reshape",false),
+ allow_reorder("allow_reorder", false),
+ drag_indicator_color("drag_indicator_color"),
no_items_text("no_items_text")
{};
void LLFlatListView::reshape(S32 width, S32 height, bool called_from_parent /* = true */)
{
S32 delta = height - getRect().getHeight();
+ LLRect visible_rc;
+ LLRect selected_rc = getLastSelectedItemRect();
+ bool keep_selection_visible = false;
+ if (delta != 0 && mKeepSelectionVisibleOnReshape && selected_rc.isValid())
+ {
+ visible_rc = getVisibleContentRect();
+ keep_selection_visible = visible_rc.overlaps(selected_rc);
+ }
+
LLScrollContainer::reshape(width, height, called_from_parent);
setItemsNoScrollWidth(width);
rearrangeItems();
- if(delta!= 0 && mKeepSelectionVisibleOnReshape)
+ if(keep_selection_visible)
{
ensureSelectedVisible();
}
@@ -91,7 +110,7 @@ bool LLFlatListView::addItem(LLPanel * item, const LLSD& value /*= LLUUID::null*
}
//_4 is for MASK
- item->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4));
+ item->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4, CLICK_LEFT));
item->setRightMouseDownCallback(boost::bind(&LLFlatListView::onItemRightMouseClick, this, new_pair, _4));
// Children don't accept the focus
@@ -142,7 +161,7 @@ bool LLFlatListView::addItemPairs(pairs_list_t panel_list, bool rearrange /*= tr
mItemsPanel->addChild(panel);
//_4 is for MASK
- panel->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4));
+ panel->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4, CLICK_LEFT));
panel->setRightMouseDownCallback(boost::bind(&LLFlatListView::onItemRightMouseClick, this, new_pair, _4));
// Children don't accept the focus
panel->setTabStop(false);
@@ -165,7 +184,7 @@ bool LLFlatListView::addItemPairs(pairs_list_t panel_list, bool rearrange /*= tr
mItemsPanel->addChild(panel);
//_4 is for MASK
- panel->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, item_pair, _4));
+ panel->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, item_pair, _4, CLICK_LEFT));
panel->setRightMouseDownCallback(boost::bind(&LLFlatListView::onItemRightMouseClick, this, item_pair, _4));
// Children don't accept the focus
panel->setTabStop(false);
@@ -217,7 +236,7 @@ bool LLFlatListView::insertItemAfter(LLPanel* after_item, LLPanel* item_to_add,
}
//_4 is for MASK
- item_to_add->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4));
+ item_to_add->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4, CLICK_LEFT));
item_to_add->setRightMouseDownCallback(boost::bind(&LLFlatListView::onItemRightMouseClick, this, new_pair, _4));
rearrangeItems();
@@ -396,6 +415,11 @@ U32 LLFlatListView::size(const bool only_visible_items) const
void LLFlatListView::clear()
{
+ if (mReorderDragPair)
+ {
+ cancelReorderDrag();
+ }
+
// This will clear mSelectedItemPairs, calling all appropriate callbacks.
resetSelection();
@@ -460,6 +484,16 @@ LLFlatListView::LLFlatListView(const LLFlatListView::Params& p)
, mIsConsecutiveSelection(false)
, mKeepSelectionVisibleOnReshape(p.keep_selection_visible_on_reshape)
, mFocusOnItemClicked(true)
+ , mAllowReorder(p.allow_reorder)
+ , mIsReordering(false)
+ , mReorderDragPair(nullptr)
+ , mDeferredSelectPair(nullptr)
+ , mReorderMouseDownX(0)
+ , mReorderMouseDownY(0)
+ , mReorderInsertIndex(-1)
+ , mDragIndicatorColor(p.drag_indicator_color.isProvided()
+ ? p.drag_indicator_color()
+ : LLUIColorTable::instance().getColor("EmphasisColor"))
{
mBorderThickness = getBorderWidth();
@@ -520,12 +554,29 @@ LLFlatListView::~LLFlatListView()
// virtual
void LLFlatListView::draw()
{
+ // Keep scrolling while a reorder drag rests over the top/bottom edge, so the
+ // user does not have to drop, scroll, and grab again.
+ if (mIsReordering)
+ {
+ S32 local_x, local_y;
+ LLUI::getInstance()->getMousePositionLocal(this, &local_x, &local_y);
+ if (autoScroll(local_x, local_y))
+ {
+ mReorderInsertIndex = constrainInsertIndex(getInsertIndexAt(local_x, local_y));
+ }
+ }
+
// Highlight border if a child of this container has keyboard focus
if( mSelectedItemsBorder->getVisible() )
{
mSelectedItemsBorder->setKeyboardFocusHighlight( hasFocus() );
}
LLScrollContainer::draw();
+
+ if (mIsReordering && mReorderInsertIndex >= 0)
+ {
+ drawReorderIndicator();
+ }
}
// virtual
@@ -602,7 +653,7 @@ void LLFlatListView::rearrangeItems()
mSelectedItemsBorder->setRect(getLastSelectedItemRect().stretch(-1));
}
-void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask)
+void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask, EMouseClickType clicktype)
{
if (!item_pair) return;
@@ -617,6 +668,11 @@ void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask)
setFocus(true);
}
+ if (clicktype == CLICK_LEFT && !(mask & (MASK_SHIFT | MASK_CONTROL)))
+ {
+ armReorderDrag(item_pair);
+ }
+
bool select_item = !isSelected(item_pair);
//*TODO find a better place for that enforcing stuff
@@ -695,6 +751,16 @@ void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask)
return;
}
+ // When a reorderable list has a multi-selection, a plain click on one of the
+ // selected rows must not collapse the selection yet: the user may be about to
+ // drag the whole selection. Defer collapsing to mouse-up if no drag happens.
+ if (mAllowReorder && mMultipleSelection && !(mask & (MASK_CONTROL | MASK_SHIFT))
+ && isSelected(item_pair) && numSelected() > 1)
+ {
+ mDeferredSelectPair = item_pair;
+ return;
+ }
+
//no need to do additional commit on selection reset
if (!(mask & MASK_CONTROL) || !mMultipleSelection) resetSelection(true);
@@ -715,8 +781,387 @@ void LLFlatListView::onItemRightMouseClick(item_pair_t* item_pair, MASK mask)
if ( !(mask & MASK_CONTROL) && mMultipleSelection && isSelected(item_pair) )
return;
- // else got same behavior as at onItemMouseClick
- onItemMouseClick(item_pair, mask);
+ // else got same behavior as at onItemMouseClick, but a right click must never start a drag
+ onItemMouseClick(item_pair, mask, CLICK_RIGHT);
+}
+
+static constexpr S32 REORDER_DRAG_THRESHOLD = 5;
+
+void LLFlatListView::armReorderDrag(item_pair_t* item_pair)
+{
+ if (!mAllowReorder)
+ {
+ return;
+ }
+
+ if (!item_pair || !item_pair->first)
+ {
+ return;
+ }
+
+ if (size() < 2)
+ {
+ return; // nothing to reorder against
+ }
+
+ if (gFocusMgr.getMouseCapture() && !hasMouseCapture())
+ {
+ return;
+ }
+
+ mReorderDragPair = item_pair;
+ mReorderDragGroup.clear();
+ mDeferredSelectPair = nullptr;
+ mIsReordering = false;
+ mReorderInsertIndex = -1;
+
+ LLUI::getInstance()->getMousePositionLocal(this, &mReorderMouseDownX, &mReorderMouseDownY);
+ gFocusMgr.setMouseCapture(this);
+}
+
+void LLFlatListView::updateReorderDrag(S32 x, S32 y)
+{
+ if (!mReorderDragPair)
+ return;
+
+ if (!mIsReordering)
+ {
+ if (abs(y - mReorderMouseDownY) < REORDER_DRAG_THRESHOLD
+ && abs(x - mReorderMouseDownX) < REORDER_DRAG_THRESHOLD)
+ {
+ return; // still within the click slop, not a drag yet
+ }
+ mIsReordering = true;
+ buildReorderGroup();
+ }
+
+ mReorderInsertIndex = constrainInsertIndex(getInsertIndexAt(x, y));
+}
+
+void LLFlatListView::buildReorderGroup()
+{
+ mReorderDragGroup.clear();
+
+ bool drag_selection = mMultipleSelection && isSelected(mReorderDragPair) && numSelected() > 1;
+
+ for (item_pair_t* pair : mItemPairs)
+ {
+ if (!pair->first->getVisible())
+ continue;
+
+ if (pair == mReorderDragPair)
+ {
+ mReorderDragGroup.push_back(pair);
+ }
+ else if (drag_selection && isSelected(pair)
+ && (!mReorderValidateCallback || mReorderValidateCallback(mReorderDragPair->second, pair->second)))
+ {
+ // only carry along selected rows that belong to the grabbed row's group
+ mReorderDragGroup.push_back(pair);
+ }
+ }
+}
+
+bool LLFlatListView::isInReorderGroup(item_pair_t* item_pair) const
+{
+ return std::find(mReorderDragGroup.begin(), mReorderDragGroup.end(), item_pair) != mReorderDragGroup.end();
+}
+
+void LLFlatListView::getReorderRemaining(pairs_list_t& remaining) const
+{
+ remaining.clear();
+ for (item_pair_t* pair : mItemPairs)
+ {
+ if (!pair->first->getVisible() || isInReorderGroup(pair))
+ continue;
+ remaining.push_back(pair);
+ }
+}
+
+void LLFlatListView::finishReorderDrag()
+{
+ if (mReorderDragPair && mIsReordering && mReorderInsertIndex >= 0)
+ {
+ pairs_list_t remaining;
+ getReorderRemaining(remaining);
+
+ // Resolve the drop boundary to the remaining row it lands before (nullptr = append).
+ item_pair_t* anchor = nullptr;
+ S32 cur = 0;
+ for (item_pair_t* pair : remaining)
+ {
+ if (cur == mReorderInsertIndex)
+ {
+ anchor = pair;
+ break;
+ }
+ ++cur;
+ }
+
+ LLSD moved_value = mReorderDragPair->second;
+
+ for (item_pair_t* pair : mReorderDragGroup)
+ {
+ mItemPairs.remove(pair);
+ }
+
+ pairs_iterator_t it = (anchor != nullptr)
+ ? std::find(mItemPairs.begin(), mItemPairs.end(), anchor)
+ : mItemPairs.end();
+ for (item_pair_t* pair : mReorderDragGroup)
+ {
+ mItemPairs.insert(it, pair); // preserves group order; it stays before anchor
+ }
+
+ rearrangeItems();
+
+ if (mReorderCallback)
+ {
+ S32 visible_index = 0;
+ for (item_pair_t* pair : mItemPairs)
+ {
+ if (!pair->first->getVisible())
+ continue;
+ if (pair == mReorderDragPair)
+ break;
+ ++visible_index;
+ }
+
+ mReorderCallback(moved_value, visible_index);
+ }
+ }
+
+ cancelReorderDrag();
+}
+
+void LLFlatListView::cancelReorderDrag()
+{
+ if (hasMouseCapture())
+ {
+ gFocusMgr.setMouseCapture(nullptr);
+ }
+
+ clearReorderDragState();
+}
+
+void LLFlatListView::clearReorderDragState()
+{
+ if (mIsReordering)
+ {
+ getWindow()->setCursor(UI_CURSOR_ARROW);
+ }
+
+ bool was_armed = (mReorderDragPair != nullptr);
+
+ mReorderDragPair = nullptr;
+ mReorderDragGroup.clear();
+ mDeferredSelectPair = nullptr;
+ mIsReordering = false;
+ mReorderInsertIndex = -1;
+
+ if (was_armed && mReorderEndedCallback)
+ {
+ mReorderEndedCallback();
+ }
+}
+
+void LLFlatListView::onMouseCaptureLost()
+{
+ clearReorderDragState();
+}
+
+void LLFlatListView::onVisibilityChange(bool new_visibility)
+{
+ if (!new_visibility && mReorderDragPair)
+ {
+ cancelReorderDrag();
+ }
+
+ LLScrollContainer::onVisibilityChange(new_visibility);
+}
+
+S32 LLFlatListView::getInsertIndexAt(S32 x, S32 y) const
+{
+ S32 panel_x, panel_y;
+ localPointToOtherView(x, y, &panel_x, &panel_y, mItemsPanel);
+
+ // The drop boundary among the remaining rows is the count of remaining rows
+ // whose vertical centre sits above the cursor.
+ S32 index = 0;
+ for (item_pair_t* pair : mItemPairs)
+ {
+ if (!pair->first->getVisible() || isInReorderGroup(pair))
+ continue;
+
+ if (pair->first->getRect().getCenterY() > panel_y)
+ {
+ ++index;
+ }
+ }
+ return index;
+}
+
+LLFlatListView::item_pair_t* LLFlatListView::getReorderPairAt(S32 x, S32 y) const
+{
+ S32 panel_x, panel_y;
+ localPointToOtherView(x, y, &panel_x, &panel_y, mItemsPanel);
+
+ for (item_pair_t* pair : mItemPairs)
+ {
+ if (!pair->first->getVisible())
+ continue;
+
+ // Claim the padding above each row so gap presses still resolve to a row.
+ LLRect rc = pair->first->getRect();
+ rc.mTop += mItemPad;
+ if (rc.pointInRect(panel_x, panel_y))
+ {
+ return pair;
+ }
+ }
+ return nullptr;
+}
+
+S32 LLFlatListView::constrainInsertIndex(S32 dest_index) const
+{
+ if (!mReorderValidateCallback)
+ return dest_index;
+
+ // Clamp the boundary to the contiguous run of remaining rows that share the
+ // grabbed row's group, so a drag can't leave its group.
+ pairs_list_t remaining;
+ getReorderRemaining(remaining);
+
+ const LLSD& dragged = mReorderDragPair->second;
+ S32 first_valid = -1;
+ S32 last_valid = -1;
+ S32 i = 0;
+ for (item_pair_t* pair : remaining)
+ {
+ if (mReorderValidateCallback(dragged, pair->second))
+ {
+ if (first_valid < 0)
+ first_valid = i;
+ last_valid = i;
+ }
+ ++i;
+ }
+
+ if (first_valid < 0)
+ return -1; // no valid neighbour (whole group is being dragged)
+
+ if (dest_index < first_valid)
+ return first_valid;
+ if (dest_index > last_valid + 1)
+ return last_valid + 1;
+ return dest_index;
+}
+
+void LLFlatListView::drawReorderIndicator()
+{
+ pairs_list_t remaining;
+ getReorderRemaining(remaining);
+ if (remaining.empty())
+ return;
+
+ const LLRect& panel_rc = mItemsPanel->getRect();
+ const LLColor4& color = mDragIndicatorColor.get();
+
+ // faint highlight on each row being moved
+ for (item_pair_t* pair : mReorderDragGroup)
+ {
+ const LLRect& dr = pair->first->getRect();
+ gl_rect_2d(panel_rc.mLeft + dr.mLeft, panel_rc.mBottom + dr.mTop,
+ panel_rc.mLeft + dr.mRight, panel_rc.mBottom + dr.mBottom,
+ color % 0.15f, true);
+ }
+
+ // insertion line at the drop boundary among the remaining rows
+ S32 count = (S32)remaining.size();
+ bool at_end = mReorderInsertIndex >= count;
+ S32 anchor_idx = at_end ? count - 1 : mReorderInsertIndex;
+
+ item_pair_t* anchor = nullptr;
+ S32 cur = 0;
+ for (item_pair_t* pair : remaining)
+ {
+ if (cur == anchor_idx)
+ {
+ anchor = pair;
+ break;
+ }
+ ++cur;
+ }
+ if (!anchor)
+ return;
+
+ const LLRect& item_rc = anchor->first->getRect();
+ S32 left = panel_rc.mLeft + item_rc.mLeft;
+ S32 right = panel_rc.mLeft + item_rc.mRight;
+ S32 line_y = panel_rc.mBottom + (at_end ? item_rc.mBottom : item_rc.mTop);
+
+ if (line_y < 0 || line_y > getRect().getHeight())
+ return;
+
+ gl_rect_2d(left, line_y, right, line_y - 1, color, true);
+}
+
+bool LLFlatListView::handleHover(S32 x, S32 y, MASK mask)
+{
+ if (mAllowReorder && mReorderDragPair && hasMouseCapture())
+ {
+ updateReorderDrag(x, y);
+ if (mIsReordering)
+ {
+ getWindow()->setCursor(UI_CURSOR_ARROWDRAG);
+ return true;
+ }
+ }
+ return LLScrollContainer::handleHover(x, y, mask);
+}
+
+bool LLFlatListView::handleMouseDown(S32 x, S32 y, MASK mask)
+{
+ bool handled = LLScrollContainer::handleMouseDown(x, y, mask);
+
+ // A press in the padding between rows misses every item, so the per-item
+ // mouse-down never arms a reorder. Map it to the nearest row and arm there;
+ // armReorderDrag's capture guard ignores presses a child control already took.
+ if (mAllowReorder && !mReorderDragPair && !(mask & (MASK_CONTROL | MASK_SHIFT)))
+ {
+ if (item_pair_t* pair = getReorderPairAt(x, y))
+ {
+ onItemMouseClick(pair, mask, CLICK_LEFT);
+ }
+ }
+
+ return handled || (mReorderDragPair != nullptr);
+}
+
+bool LLFlatListView::handleMouseUp(S32 x, S32 y, MASK mask)
+{
+ if (mReorderDragPair && hasMouseCapture())
+ {
+ bool was_reordering = mIsReordering;
+ item_pair_t* deferred = mDeferredSelectPair;
+ mDeferredSelectPair = nullptr;
+
+ finishReorderDrag();
+
+ if (was_reordering)
+ {
+ return true; // a drag happened: keep the (multi-)selection on the moved rows
+ }
+
+ // No drag: apply the selection collapse that was deferred on mouse-down.
+ if (deferred)
+ {
+ resetSelection(true);
+ selectItemPair(deferred, true);
+ return true;
+ }
+ }
+ return LLScrollContainer::handleMouseUp(x, y, mask);
}
bool LLFlatListView::handleKeyHere(KEY key, MASK mask)
@@ -1087,6 +1532,18 @@ bool LLFlatListView::removeItemPair(item_pair_t* item_pair, bool rearrange)
{
llassert(item_pair);
+ bool removing_reorder_pair = item_pair == mReorderDragPair
+ || std::find(mReorderDragGroup.begin(), mReorderDragGroup.end(), item_pair) != mReorderDragGroup.end();
+
+ if (removing_reorder_pair)
+ {
+ cancelReorderDrag();
+ }
+ if (item_pair == mDeferredSelectPair)
+ {
+ mDeferredSelectPair = nullptr;
+ }
+
bool deleted = false;
bool selection_changed = false;
for (pairs_iterator_t it = mItemPairs.begin(); it != mItemPairs.end(); ++it)