From 9aaf3e7747dd92ecdbce9878d3ac19a0027845c4 Mon Sep 17 00:00:00 2001 From: gwigz Date: Thu, 25 Jun 2026 20:08:15 +0100 Subject: @5929 Add drag-n-drop reordering for clothing layers (#5918) - Drag-and-drop reordering for clothing layers with visual insertion indicator - Includes new `keep_scroll_pos` param to preserve and restore scroll positions during layout and resize --- indra/llui/llflatlistview.cpp | 450 ++++++++++++++++++++- indra/llui/llflatlistview.h | 78 +++- indra/llui/llscrollcontainer.cpp | 33 ++ indra/llui/llscrollcontainer.h | 5 +- indra/newview/llagentwearables.cpp | 46 ++- indra/newview/llagentwearables.h | 1 + indra/newview/llappearancemgr.cpp | 125 +++--- indra/newview/llappearancemgr.h | 13 + indra/newview/llcofwearables.cpp | 66 ++- indra/newview/llcofwearables.h | 9 + .../skins/default/xui/en/panel_cof_wearables.xml | 2 + indra/newview/skins/default/xui/en/strings.xml | 1 + 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 + static const LLDefaultChildRegistry::Register 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 item_pad; + /** allow items to be reordered by dragging them with the mouse */ + Optional allow_reorder; + + /** colour of the insertion indicator drawn while reordering */ + Optional drag_indicator_color; + /** textbox with info message when list is empty*/ Optional no_items_text; Params(); }; + /** Fired after the user drags an item to a new position: (moved value, new visible index). */ + typedef boost::function 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 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 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 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 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 min_auto_scroll_rate, max_auto_scroll_rate; Optional 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 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 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 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"> Toy Camera + Drag to reorder layers, or use the arrows Person (no name) Owner: -- cgit v1.3