summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorProfeta <allefalvessilva2011@hotmail.com>2026-06-25 16:18:52 -0300
committerGitHub <noreply@github.com>2026-06-25 16:18:52 -0300
commit621a2d8af9b6da91c9f2ccba133fa9aaa6ccdf9b (patch)
tree88aa367b376f7037ece073b47ea3561f59be71ab
parentb7600174a256dce37e274014853974aedc372154 (diff)
parent9aaf3e7747dd92ecdbce9878d3ac19a0027845c4 (diff)
Merge branch 'develop' into Add-localized-Debug-Settings
-rw-r--r--indra/llui/llflatlistview.cpp450
-rw-r--r--indra/llui/llflatlistview.h78
-rw-r--r--indra/llui/llscrollcontainer.cpp33
-rw-r--r--indra/llui/llscrollcontainer.h5
-rw-r--r--indra/newview/llagentwearables.cpp46
-rw-r--r--indra/newview/llagentwearables.h1
-rw-r--r--indra/newview/llappearancemgr.cpp125
-rw-r--r--indra/newview/llappearancemgr.h13
-rw-r--r--indra/newview/llcofwearables.cpp66
-rw-r--r--indra/newview/llcofwearables.h9
-rw-r--r--indra/newview/skins/default/xui/en/panel_cof_wearables.xml2
-rw-r--r--indra/newview/skins/default/xui/en/strings.xml1
12 files changed, 769 insertions, 60 deletions
diff --git a/indra/llui/llflatlistview.cpp b/indra/llui/llflatlistview.cpp
index 34eb1ea3fc..64e47cea6e 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(NULL)
+ , mDeferredSelectPair(NULL)
+ , 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,364 @@ 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 const 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 = NULL;
+ 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 (NULL = append).
+ item_pair_t* anchor = NULL;
+ 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 != NULL)
+ ? 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(NULL);
+ }
+
+ clearReorderDragState();
+}
+
+void LLFlatListView::clearReorderDragState()
+{
+ if (mIsReordering)
+ {
+ getWindow()->setCursor(UI_CURSOR_ARROW);
+ }
+
+ bool was_armed = (mReorderDragPair != NULL);
+
+ mReorderDragPair = NULL;
+ mReorderDragGroup.clear();
+ mDeferredSelectPair = NULL;
+ 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 NULL;
+}
+
+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 = NULL;
+ 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 != NULL);
+}
+
+bool LLFlatListView::handleMouseUp(S32 x, S32 y, MASK mask)
+{
+ if (mReorderDragPair && hasMouseCapture())
+ {
+ bool was_reordering = mIsReordering;
+ item_pair_t* deferred = mDeferredSelectPair;
+ mDeferredSelectPair = NULL;
+
+ 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 +1509,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 = NULL;
+ }
+
bool deleted = false;
bool selection_changed = false;
for (pairs_iterator_t it = mItemPairs.begin(); it != mItemPairs.end(); ++it)
diff --git a/indra/llui/llflatlistview.h b/indra/llui/llflatlistview.h
index 39afa33be8..ae20a4b1eb 100644
--- a/indra/llui/llflatlistview.h
+++ b/indra/llui/llflatlistview.h
@@ -30,6 +30,7 @@
#include "llpanel.h"
#include "llscrollcontainer.h"
#include "lltextbox.h"
+#include "lluicolor.h"
/**
@@ -106,12 +107,28 @@ public:
/** padding between items */
Optional<U32> item_pad;
+ /** allow items to be reordered by dragging them with the mouse */
+ Optional<bool> allow_reorder;
+
+ /** colour of the insertion indicator drawn while reordering */
+ Optional<LLUIColor> drag_indicator_color;
+
/** textbox with info message when list is empty*/
Optional<LLTextBox::Params> no_items_text;
Params();
};
+ /** Fired after the user drags an item to a new position: (moved value, new visible index). */
+ typedef boost::function<void(const LLSD& value, S32 new_index)> reorder_signal_t;
+
+ /**
+ * Returns true if the dragged item is allowed to move past the given neighbour.
+ * Lets owners constrain reordering (e.g. keep items within a group). Optional;
+ * when unset any reorder is permitted.
+ */
+ typedef boost::function<bool(const LLSD& dragged, const LLSD& neighbour)> reorder_validate_signal_t;
+
// disable traversal when finding widget to hand focus off to
/*virtual*/ bool canFocusChildren() const override { return false; }
@@ -259,6 +276,18 @@ public:
/** Turn on/off selection support */
void setAllowSelection(bool can_select) { mAllowSelection = can_select; }
+ /** Turn on/off drag-to-reorder support */
+ void setAllowReorder(bool allow) { mAllowReorder = allow; }
+
+ void setReorderCallback(reorder_signal_t cb) { mReorderCallback = cb; }
+ void setReorderValidateCallback(reorder_validate_signal_t cb) { mReorderValidateCallback = cb; }
+
+ /** Fired when a reorder grab is released, whether or not it moved anything */
+ void setReorderEndedCallback(boost::function<void()> cb) { mReorderEndedCallback = cb; }
+
+ /** True while an item is grabbed for a drag-to-reorder (before or during the drag) */
+ bool isReorderActive() const { return mReorderDragPair != NULL; }
+
/** Sets flag whether onCommit should be fired if selection was changed */
// FIXME: this should really be a separate signal, since "Commit" implies explicit user action, and selection changes can happen more indirectly.
void setCommitOnSelectionChange(bool b) { mCommitOnSelectionChange = b; }
@@ -330,7 +359,7 @@ protected:
LLFlatListView(const LLFlatListView::Params& p);
/** Manage selection on mouse events */
- void onItemMouseClick(item_pair_t* item_pair, MASK mask);
+ void onItemMouseClick(item_pair_t* item_pair, MASK mask, EMouseClickType clicktype);
void onItemRightMouseClick(item_pair_t* item_pair, MASK mask);
@@ -368,6 +397,16 @@ protected:
virtual bool handleKeyHere(KEY key, MASK mask) override;
+ virtual bool handleHover(S32 x, S32 y, MASK mask) override;
+
+ virtual bool handleMouseDown(S32 x, S32 y, MASK mask) override;
+
+ virtual bool handleMouseUp(S32 x, S32 y, MASK mask) override;
+
+ virtual void onMouseCaptureLost() override;
+
+ virtual void onVisibilityChange(bool new_visibility) override;
+
virtual bool postBuild() override;
virtual void onFocusReceived() override;
@@ -384,6 +423,29 @@ protected:
private:
+ // Drag-to-reorder helpers.
+ void armReorderDrag(item_pair_t* item_pair);
+ void updateReorderDrag(S32 x, S32 y);
+ void finishReorderDrag();
+ void cancelReorderDrag();
+ void clearReorderDragState();
+
+ // Builds the set of pairs being dragged (the whole selection when the grabbed
+ // row is part of a multi-selection, constrained to the validator's group).
+ void buildReorderGroup();
+ bool isInReorderGroup(item_pair_t* item_pair) const;
+
+ // The remaining visible pairs (those not being dragged), in visual order.
+ void getReorderRemaining(pairs_list_t& remaining) const;
+ // Number of leading non-dragged items whose centre sits above (x, y).
+ S32 getInsertIndexAt(S32 x, S32 y) const;
+ // Row under (x, y), counting the padding above each row, so presses in the
+ // inter-row gaps still map to a row instead of falling through.
+ item_pair_t* getReorderPairAt(S32 x, S32 y) const;
+ // Clamps an insertion boundary to the validator's contiguous group.
+ S32 constrainInsertIndex(S32 dest_index) const;
+ void drawReorderIndicator();
+
void setItemsNoScrollWidth(S32 new_width) {mItemsNoScrollWidth = new_width - 2 * mBorderThickness;}
void setNoItemsCommentVisible(bool visible) const;
@@ -427,6 +489,20 @@ private:
bool mFocusOnItemClicked;
+ /** Drag-to-reorder state */
+ bool mAllowReorder;
+ bool mIsReordering; // a drag is currently in progress
+ item_pair_t* mReorderDragPair; // the grabbed pair (drag anchor)
+ pairs_list_t mReorderDragGroup; // all pairs being dragged, in visual order
+ item_pair_t* mDeferredSelectPair; // collapse selection to this on mouse-up if no drag
+ S32 mReorderMouseDownX;
+ S32 mReorderMouseDownY;
+ S32 mReorderInsertIndex; // current drop boundary among the remaining (non-dragged) items
+ LLUIColor mDragIndicatorColor;
+ reorder_signal_t mReorderCallback;
+ reorder_validate_signal_t mReorderValidateCallback;
+ boost::function<void()> mReorderEndedCallback;
+
/** All pairs of the list */
pairs_list_t mItemPairs;
diff --git a/indra/llui/llscrollcontainer.cpp b/indra/llui/llscrollcontainer.cpp
index e36fd45bb4..ad2f58db69 100644
--- a/indra/llui/llscrollcontainer.cpp
+++ b/indra/llui/llscrollcontainer.cpp
@@ -75,6 +75,7 @@ LLScrollContainer::Params::Params()
max_auto_scroll_rate("max_auto_scroll_rate", 1000),
max_auto_scroll_zone("max_auto_scroll_zone", 16),
reserve_scroll_corner("reserve_scroll_corner", false),
+ keep_scroll_pos("keep_scroll_pos", false),
size("size", -1)
{}
@@ -93,8 +94,12 @@ LLScrollContainer::LLScrollContainer(const LLScrollContainer::Params& p)
mMaxAutoScrollRate(p.max_auto_scroll_rate),
mMaxAutoScrollZone(p.max_auto_scroll_zone),
mScrolledView(NULL),
+ mKeepScrollPos(p.keep_scroll_pos),
mSize(p.size)
{
+ mStoredDocPos[VERTICAL] = 0;
+ mStoredDocPos[HORIZONTAL] = 0;
+
static LLUICachedControl<S32> scrollbar_size_control ("UIScrollbarSize", 0);
S32 scrollbar_size = (mSize == -1 ? scrollbar_size_control : mSize);
@@ -605,6 +610,22 @@ void LLScrollContainer::updateScroll()
calcVisibleSize( &visible_width, &visible_height, &show_h_scrollbar, &show_v_scrollbar );
S32 border_width = getBorderWidth();
+
+ // Remember the position only while the scrollbar is genuinely showing scrollable
+ // content, so a transient empty pass (e.g. a list clearing before it repopulates)
+ // cannot overwrite it with 0.
+ if (mKeepScrollPos)
+ {
+ if (show_v_scrollbar && mScrollbar[VERTICAL]->getVisible())
+ {
+ mStoredDocPos[VERTICAL] = mScrollbar[VERTICAL]->getDocPos();
+ }
+ if (show_h_scrollbar && mScrollbar[HORIZONTAL]->getVisible())
+ {
+ mStoredDocPos[HORIZONTAL] = mScrollbar[HORIZONTAL]->getDocPos();
+ }
+ }
+
if( show_v_scrollbar )
{
if( doc_rect.mTop < getRect().getHeight() - border_width )
@@ -673,6 +694,18 @@ void LLScrollContainer::updateScroll()
mScrollbar[VERTICAL]->setDocSize( doc_height );
mScrollbar[VERTICAL]->setPageSize( visible_height );
+
+ if (mKeepScrollPos)
+ {
+ if (show_v_scrollbar)
+ {
+ mScrollbar[VERTICAL]->setDocPos(mStoredDocPos[VERTICAL]);
+ }
+ if (show_h_scrollbar)
+ {
+ mScrollbar[HORIZONTAL]->setDocPos(mStoredDocPos[HORIZONTAL]);
+ }
+ }
} // end updateScroll
void LLScrollContainer::setBorderVisible(bool b)
diff --git a/indra/llui/llscrollcontainer.h b/indra/llui/llscrollcontainer.h
index 859750d71c..7761824dbb 100644
--- a/indra/llui/llscrollcontainer.h
+++ b/indra/llui/llscrollcontainer.h
@@ -64,7 +64,8 @@ public:
reserve_scroll_corner,
border_visible,
hide_scrollbar,
- ignore_arrow_keys;
+ ignore_arrow_keys,
+ keep_scroll_pos;
Optional<F32> min_auto_scroll_rate,
max_auto_scroll_rate;
Optional<U32> max_auto_scroll_zone;
@@ -138,6 +139,8 @@ private:
void calcVisibleSize( S32 *visible_width, S32 *visible_height, bool* show_h_scrollbar, bool* show_v_scrollbar ) const;
LLScrollbar* mScrollbar[ORIENTATION_COUNT];
+ S32 mStoredDocPos[ORIENTATION_COUNT];
+ bool mKeepScrollPos;
S32 mSize;
bool mIsOpaque;
LLUIColor mBackgroundColor;
diff --git a/indra/newview/llagentwearables.cpp b/indra/newview/llagentwearables.cpp
index a075b6f004..edd08c19e5 100644
--- a/indra/newview/llagentwearables.cpp
+++ b/indra/newview/llagentwearables.cpp
@@ -547,14 +547,23 @@ const S32 LLAgentWearables::getWearableIdxFromItem(const LLViewerInventoryItem*
U32 wearable_count = getWearableCount(type);
if (0 == wearable_count) return -1;
- const LLUUID& asset_id = item->getAssetUUID();
+ // Match by linked inventory item id so the correct copy is found when several
+ // worn wearables of this type share the same asset.
+ const LLUUID item_id = gInventory.getLinkedItemID(item->getUUID());
+ for (U32 i = 0; i < wearable_count; ++i)
+ {
+ const LLViewerWearable* wearable = getViewerWearable(type, i);
+ if (!wearable) continue;
+ if (wearable->getItemID() == item_id) return i;
+ }
+ // Fall back to asset id for items whose inventory id can't be resolved.
+ const LLUUID& asset_id = item->getAssetUUID();
for (U32 i = 0; i < wearable_count; ++i)
{
const LLViewerWearable* wearable = getViewerWearable(type, i);
if (!wearable) continue;
- if (wearable->getAssetID() != asset_id) continue;
- return i;
+ if (wearable->getAssetID() == asset_id) return i;
}
return -1;
@@ -1529,6 +1538,37 @@ bool LLAgentWearables::moveWearable(const LLViewerInventoryItem* item, bool clos
return false;
}
+bool LLAgentWearables::moveWearableToIndex(const LLViewerInventoryItem* item, U32 new_index)
+{
+ if (!item) return false;
+ if (!item->isWearableType()) return false;
+
+ LLWearableType::EType type = item->getWearableType();
+ U32 wearable_count = getWearableCount(type);
+ if (wearable_count < 2) return false;
+
+ if (new_index >= wearable_count) new_index = wearable_count - 1;
+
+ S32 cur = getWearableIdxFromItem(item);
+ if (cur < 0) return false;
+ if ((U32)cur == new_index) return true; // already in place
+
+ // step the wearable to its new slot one swap at a time
+ U32 pos = (U32)cur;
+ while (pos < new_index)
+ {
+ if (!swapWearables(type, pos, pos + 1)) return false;
+ ++pos;
+ }
+ while (pos > new_index)
+ {
+ if (!swapWearables(type, pos, pos - 1)) return false;
+ --pos;
+ }
+
+ return true;
+}
+
// static
void LLAgentWearables::createWearable(LLWearableType::EType type, bool wear, const LLUUID& parent_id, std::function<void(const LLUUID&)> created_cb)
{
diff --git a/indra/newview/llagentwearables.h b/indra/newview/llagentwearables.h
index 70da86805c..3c9ffa7443 100644
--- a/indra/newview/llagentwearables.h
+++ b/indra/newview/llagentwearables.h
@@ -129,6 +129,7 @@ public:
static void createWearable(LLWearableType::EType type, bool wear = false, const LLUUID& parent_id = LLUUID::null, std::function<void(const LLUUID&)> created_cb = nullptr);
static void editWearable(const LLUUID& item_id);
bool moveWearable(const LLViewerInventoryItem* item, bool closer_to_body);
+ bool moveWearableToIndex(const LLViewerInventoryItem* item, U32 new_index);
void requestEditingWearable(const LLUUID& item_id);
void editWearableIfRequested(const LLUUID& item_id);
diff --git a/indra/newview/llappearancemgr.cpp b/indra/newview/llappearancemgr.cpp
index 4f549314af..80a3344979 100644
--- a/indra/newview/llappearancemgr.cpp
+++ b/indra/newview/llappearancemgr.cpp
@@ -4269,77 +4269,110 @@ bool LLAppearanceMgr::moveWearable(LLViewerInventoryItem* item, bool closer_to_b
{
if (!item || !item->isWearableType()) return false;
if (item->getType() != LLAssetType::AT_CLOTHING) return false;
- if (!gInventory.isObjectDescendentOf(item->getUUID(), getCOF())) return false;
S32 pos = gAgentWearables.getWearableIdxFromItem(item);
if (pos < 0) return false; // Not found
+ if (closer_to_body && pos == 0) return false; // already closest to the body
+
+ return reorderWearable(item, closer_to_body ? (U32)(pos - 1) : (U32)(pos + 1));
+}
+
+bool LLAppearanceMgr::reorderWearable(LLViewerInventoryItem* item, U32 new_index)
+{
+ if (!item || !item->isWearableType()) return false;
+ if (item->getType() != LLAssetType::AT_CLOTHING) return false;
+ if (!gInventory.isObjectDescendentOf(item->getUUID(), getCOF())) return false;
+
+ LLWearableType::EType type = item->getWearableType();
+ U32 count = gAgentWearables.getWearableCount(type);
+ if (count < 2) return false; // nothing to reorder against
+
+ if (new_index >= count) new_index = count - 1;
- U32 count = gAgentWearables.getWearableCount(item->getWearableType());
- if (count < 2) return false; // Nothing to swap with
- if (closer_to_body)
+ S32 cur = gAgentWearables.getWearableIdxFromItem(item);
+ if (cur < 0) return false;
+ if ((U32)cur == new_index) return false; // already in place
+
+ // Update the live layer order first; bail before touching inventory if it fails.
+ if (!gAgentWearables.moveWearableToIndex(item, new_index)) return false;
+
+ persistWearableOrder(type);
+ return true;
+}
+
+bool LLAppearanceMgr::reorderWearableGroup(LLWearableType::EType type, const uuid_vec_t& ordered_link_ids)
+{
+ U32 count = gAgentWearables.getWearableCount(type);
+ if (count < 2) return false;
+ if (ordered_link_ids.size() != count) return false; // order must cover the whole group
+
+ // Validate everything before mutating, so a bad link can't leave it half-reordered.
+ for (const LLUUID& link_id : ordered_link_ids)
{
- if (pos == 0) return false; // already first
+ LLViewerInventoryItem* link = gInventory.getItem(link_id);
+ if (!link || link->getWearableType() != type) return false;
}
- else
+
+ // ordered_link_ids runs furthest-to-closest; body index 0 is closest to the body.
+ // Place each target at its body index, leaving already-placed lower indices untouched.
+ for (U32 body_index = 0; body_index < count; ++body_index)
{
- if (pos == count - 1) return false; // already last
+ const LLUUID& link_id = ordered_link_ids[count - 1 - body_index];
+ LLViewerInventoryItem* link = gInventory.getItem(link_id);
+ if (!link || link->getWearableType() != type) return false;
+ if (!gAgentWearables.moveWearableToIndex(link, body_index)) return false;
}
- U32 old_pos = (U32)pos;
- U32 swap_with = closer_to_body ? old_pos - 1 : old_pos + 1;
- LLUUID swap_item_id = gAgentWearables.getWearableItemID(item->getWearableType(), swap_with);
+ persistWearableOrder(type);
+ return true;
+}
+
+void LLAppearanceMgr::persistWearableOrder(LLWearableType::EType type)
+{
+ U32 count = gAgentWearables.getWearableCount(type);
- // Find link item from item id.
+ // Rewrite the sort-index descriptions for the whole type group in one pass so
+ // the order survives relog, trusting gAgentWearables over existing descriptions.
LLInventoryModel::cat_array_t cats;
LLInventoryModel::item_array_t items;
- LLFindWearablesOfType filter_wearables_of_type(item->getWearableType());
+ LLFindWearablesOfType filter_wearables_of_type(type);
gInventory.collectDescendentsIf(getCOF(), cats, items, true, filter_wearables_of_type);
- if (items.empty()) return false;
- LLViewerInventoryItem* swap_item = nullptr;
- for (auto iter : items)
+ for (U32 i = 0; i < count; ++i)
{
- if (iter->getLinkedUUID() == swap_item_id)
+ LLUUID linked_id = gAgentWearables.getWearableItemID(type, i);
+ if (linked_id.isNull()) continue;
+
+ LLViewerInventoryItem* link = nullptr;
+ for (auto iter : items)
{
- swap_item = iter.get();
- break;
+ if (iter->getLinkedUUID() == linked_id)
+ {
+ link = iter.get();
+ break;
+ }
}
- }
- if (!swap_item)
- {
- return false;
- }
+ if (!link) continue;
- // Description is supposed to hold sort index, but user could have changed
- // order rapidly and there might be a state mismatch between description
- // and gAgentWearables, trust gAgentWearables over description.
- // Generate new description.
- std::string new_desc = build_order_string(item->getWearableType(), old_pos);
- swap_item->setDescription(new_desc);
- new_desc = build_order_string(item->getWearableType(), swap_with);
- item->setDescription(new_desc);
+ std::string new_desc = build_order_string(type, i);
+ if (new_desc == link->getActualDescription()) continue;
- item->setComplete(true);
- item->updateServer(false);
- gInventory.updateItem(item);
-
- swap_item->setComplete(true);
- swap_item->updateServer(false);
- gInventory.updateItem(swap_item);
+ // Keep the local cache consistent immediately (so the COF list does not
+ // flicker back on a refresh), and persist durably via AISv3, matching
+ // updateClothingOrderingInfo() rather than the legacy UDP updateServer().
+ link->setDescription(new_desc);
+ LLSD updates;
+ updates["desc"] = new_desc;
+ update_inventory_item(link->getUUID(), updates, NULL);
+ }
- //to cause appearance of the agent to be updated
- bool result = false;
- if ((result = gAgentWearables.moveWearable(item, closer_to_body)))
+ if (isAgentAvatarValid())
{
- gAgentAvatarp->wearableUpdated(item->getWearableType());
+ gAgentAvatarp->wearableUpdated(type);
}
setOutfitDirty(true);
-
- //*TODO do we need to notify observers here in such a way?
gInventory.notifyObservers();
-
- return result;
}
//static
diff --git a/indra/newview/llappearancemgr.h b/indra/newview/llappearancemgr.h
index 9d90be0004..77146ba0be 100644
--- a/indra/newview/llappearancemgr.h
+++ b/indra/newview/llappearancemgr.h
@@ -210,6 +210,15 @@ public:
bool moveWearable(LLViewerInventoryItem* item, bool closer_to_body);
+ // Move a clothing item to an absolute layer index within its wearable type
+ // (0 == closest to the body). Persists the new order to the COF link descriptions.
+ bool reorderWearable(LLViewerInventoryItem* item, U32 new_index);
+
+ // Apply a complete layer order for one wearable type, persisting it to the
+ // COF link descriptions. ordered_link_ids lists the type's COF link items
+ // furthest-to-closest.
+ bool reorderWearableGroup(LLWearableType::EType type, const uuid_vec_t& ordered_link_ids);
+
static void sortItemsByActualDescription(LLInventoryModel::item_array_t& items);
//Divvy items into arrays by wearable type
@@ -250,6 +259,10 @@ private:
private:
+ // Rewrite COF sort-index descriptions for one wearable type to match the
+ // current in-memory layer order, then trigger a single appearance update.
+ void persistWearableOrder(LLWearableType::EType type);
+
void filterWearableItems(LLInventoryModel::item_array_t& items, S32 max_per_type, S32 max_total, bool skip_bodyparts = false);
void getDescendentsOfAssetType(const LLUUID& category,
diff --git a/indra/newview/llcofwearables.cpp b/indra/newview/llcofwearables.cpp
index 6bd15d4b61..f5d144dcc6 100644
--- a/indra/newview/llcofwearables.cpp
+++ b/indra/newview/llcofwearables.cpp
@@ -292,7 +292,8 @@ LLCOFWearables::LLCOFWearables() : LLPanel(),
mBodyPartsTab(NULL),
mLastSelectedTab(NULL),
mAccordionCtrl(NULL),
- mCOFVersion(-1)
+ mCOFVersion(-1),
+ mRefreshPending(false)
{
mClothingMenu = new CofClothingContextMenu(this);
mAttachmentMenu = new CofAttachmentContextMenu(this);
@@ -326,6 +327,10 @@ bool LLCOFWearables::postBuild()
mClothing->setCommitOnSelectionChange(true);
mBodyParts->setCommitOnSelectionChange(true);
+ mClothing->setReorderValidateCallback(boost::bind(&LLCOFWearables::canReorderClothing, this, _1, _2));
+ mClothing->setReorderCallback(boost::bind(&LLCOFWearables::onClothingReordered, this, _1, _2));
+ mClothing->setReorderEndedCallback(boost::bind(&LLCOFWearables::onReorderEnded, this));
+
//clothing is sorted according to its position relatively to the body
mAttachments->setComparator(&WEARABLE_NAME_COMPARATOR);
mBodyParts->setComparator(&WEARABLE_NAME_COMPARATOR);
@@ -377,6 +382,50 @@ void LLCOFWearables::onSelectionChange(LLFlatListView* selected_list)
onCommit();
}
+bool LLCOFWearables::canReorderClothing(const LLSD& dragged_value, const LLSD& neighbour_value)
+{
+ LLViewerInventoryItem* dragged = gInventory.getItem(dragged_value.asUUID());
+ LLViewerInventoryItem* neighbour = gInventory.getItem(neighbour_value.asUUID());
+ if (!dragged || !neighbour) return false; // reject placeholder/dummy rows
+ if (!dragged->isWearableType() || !neighbour->isWearableType()) return false;
+
+ return dragged->getWearableType() == neighbour->getWearableType();
+}
+
+void LLCOFWearables::onClothingReordered(const LLSD& dragged_value, S32 /*new_index*/)
+{
+ LLViewerInventoryItem* item = gInventory.getItem(dragged_value.asUUID());
+ if (!item || !item->isWearableType()) return;
+
+ LLWearableType::EType type = item->getWearableType();
+
+ // Persist the affected type group's full order from the list's current order
+ // (furthest-to-closest), which covers both single- and multi-row drags.
+ uuid_vec_t ordered_link_ids;
+ std::vector<LLSD> values;
+ mClothing->getValues(values);
+ for (const LLSD& value : values)
+ {
+ LLViewerInventoryItem* other = gInventory.getItem(value.asUUID());
+ if (!other || !other->isWearableType() || other->getWearableType() != type) continue;
+
+ ordered_link_ids.push_back(value.asUUID());
+ }
+
+ if (ordered_link_ids.size() < 2) return;
+
+ LLAppearanceMgr::getInstance()->reorderWearableGroup(type, ordered_link_ids);
+}
+
+void LLCOFWearables::onReorderEnded()
+{
+ // Run a refresh that arrived (and was skipped) while the drag was active.
+ if (mRefreshPending)
+ {
+ refresh();
+ }
+}
+
void LLCOFWearables::onAccordionTabStateChanged(LLUICtrl* ctrl, const LLSD& expanded)
{
bool had_selected_items = mClothing->numSelected() || mAttachments->numSelected() || mBodyParts->numSelected();
@@ -401,6 +450,15 @@ void LLCOFWearables::onAccordionTabStateChanged(LLUICtrl* ctrl, const LLSD& expa
void LLCOFWearables::refresh()
{
+ // Don't rebuild mid-drag, clearing the list would cancel the active reorder.
+ // Remember it so onReorderEnded() can re-run the skipped refresh on release.
+ if (mClothing && mClothing->isReorderActive())
+ {
+ mRefreshPending = true;
+ return;
+ }
+ mRefreshPending = false;
+
const LLUUID cof_id = LLAppearanceMgr::instance().getCOF();
if (cof_id.isNull())
{
@@ -560,6 +618,12 @@ LLPanelClothingListItem* LLCOFWearables::buildClothingListItem(LLViewerInventory
item_panel->setShowMoveUpButton(!first);
item_panel->setShowMoveDownButton(!last);
+ // hint that rows in a multi-layer group can be dragged to reorder
+ if (!(first && last))
+ {
+ item_panel->setToolTip(LLTrans::getString("ReorderClothingTooltip"));
+ }
+
//setting callbacks
//*TODO move that item panel's inner structure disclosing stuff into the panels
item_panel->childSetAction("btn_delete", boost::bind(mCOFCallbacks.mDeleteWearable));
diff --git a/indra/newview/llcofwearables.h b/indra/newview/llcofwearables.h
index b349f35921..7d4d8917b4 100644
--- a/indra/newview/llcofwearables.h
+++ b/indra/newview/llcofwearables.h
@@ -102,6 +102,12 @@ protected:
void onSelectionChange(LLFlatListView* selected_list);
void onAccordionTabStateChanged(LLUICtrl* ctrl, const LLSD& expanded);
+ // Clothing drag-to-reorder: only allow moves within the same wearable type,
+ // and translate a drop into a layer-order change.
+ bool canReorderClothing(const LLSD& dragged_value, const LLSD& neighbour_value);
+ void onClothingReordered(const LLSD& dragged_value, S32 new_index);
+ void onReorderEnded();
+
LLPanelClothingListItem* buildClothingListItem(LLViewerInventoryItem* item, bool first, bool last);
LLPanelBodyPartsListItem* buildBodypartListItem(LLViewerInventoryItem* item);
LLPanelDeletableWearableListItem* buildAttachemntListItem(LLViewerInventoryItem* item);
@@ -132,6 +138,9 @@ protected:
/* COF category version since last refresh */
S32 mCOFVersion;
+
+ /* a refresh arrived mid-drag and was skipped; re-run it when the drag ends */
+ bool mRefreshPending;
};
diff --git a/indra/newview/skins/default/xui/en/panel_cof_wearables.xml b/indra/newview/skins/default/xui/en/panel_cof_wearables.xml
index 9544042e94..37867b4a0e 100644
--- a/indra/newview/skins/default/xui/en/panel_cof_wearables.xml
+++ b/indra/newview/skins/default/xui/en/panel_cof_wearables.xml
@@ -26,10 +26,12 @@
title="Clothing">
<flat_list_view
allow_select="true"
+ allow_reorder="true"
follows="all"
height="10"
item_pad="3"
keep_selection_visible_on_reshape="true"
+ keep_scroll_pos="true"
layout="topleft"
left="0"
multi_select="true"
diff --git a/indra/newview/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml
index a8fa4b735a..e44a18ae79 100644
--- a/indra/newview/skins/default/xui/en/strings.xml
+++ b/indra/newview/skins/default/xui/en/strings.xml
@@ -225,6 +225,7 @@ If you feel this is an error, please contact support@secondlife.com</string>
<string name="Toycamera">Toy Camera</string>
<!-- Tooltip -->
+ <string name="ReorderClothingTooltip">Drag to reorder layers, or use the arrows</string> <!-- Hint shown on clothing rows that can be dragged to reorder -->
<string name="TooltipPerson">Person</string><!-- Object under mouse pointer is an avatar -->
<string name="TooltipNoName">(no name)</string> <!-- No name on an object -->
<string name="TooltipOwner">Owner:</string> <!-- Owner name follows -->