diff --git a/indra/llui/llflatlistview.cpp b/indra/llui/llflatlistview.cpp index 1ab7c2e998..a514417781 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(); } @@ -414,6 +433,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(); @@ -478,6 +502,17 @@ LLFlatListView::LLFlatListView(const LLFlatListView::Params& p) , mIsConsecutiveSelection(false) , mKeepSelectionVisibleOnReshape(p.keep_selection_visible_on_reshape) , mFocusOnItemClicked(true) + , mAllowReorder(p.allow_reorder) + , mIsReordering(false) + , mProcessingRightClick(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(); @@ -538,12 +573,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 @@ -635,6 +687,11 @@ void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask) setFocus(true); } + if (!(mask & (MASK_SHIFT | MASK_CONTROL))) + { + armReorderDrag(item_pair); + } + bool select_item = !isSelected(item_pair); //*TODO find a better place for that enforcing stuff @@ -713,6 +770,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); @@ -733,8 +800,302 @@ 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 + // else got same behavior as at onItemMouseClick, but a right click must never start a drag + mProcessingRightClick = true; onItemMouseClick(item_pair, mask); + mProcessingRightClick = false; +} + +static const S32 REORDER_DRAG_THRESHOLD = 5; + +void LLFlatListView::armReorderDrag(item_pair_t* item_pair) +{ + if (!mAllowReorder || mProcessingRightClick) + { + 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) + { + mReorderCallback(moved_value, mReorderInsertIndex); + } + } + + cancelReorderDrag(); +} + +void LLFlatListView::cancelReorderDrag() +{ + if (hasMouseCapture()) + { + gFocusMgr.setMouseCapture(NULL); + } + + clearReorderDragState(); +} + +void LLFlatListView::clearReorderDragState() +{ + if (mIsReordering) + { + getWindow()->setCursor(UI_CURSOR_ARROW); + } + mReorderDragPair = NULL; + mReorderDragGroup.clear(); + mDeferredSelectPair = NULL; + mIsReordering = false; + mReorderInsertIndex = -1; +} + +void LLFlatListView::onMouseCaptureLost() +{ + clearReorderDragState(); +} + +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; +} + +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::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) @@ -1105,6 +1466,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 6eda0cbed2..94f491a788 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 @@ class LLFlatListView : public LLScrollContainer, public LLEditMenuHandler /** 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; } @@ -265,6 +282,12 @@ class LLFlatListView : public LLScrollContainer, public LLEditMenuHandler /** 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; } + /** 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; } @@ -374,6 +397,12 @@ class LLFlatListView : public LLScrollContainer, public LLEditMenuHandler virtual bool handleKeyHere(KEY key, MASK mask) override; + virtual bool handleHover(S32 x, S32 y, MASK mask) override; + + virtual bool handleMouseUp(S32 x, S32 y, MASK mask) override; + + virtual void onMouseCaptureLost() override; + virtual bool postBuild() override; virtual void onFocusReceived() override; @@ -390,6 +419,26 @@ class LLFlatListView : public LLScrollContainer, public LLEditMenuHandler 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; + // 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; @@ -433,6 +482,20 @@ class LLFlatListView : public LLScrollContainer, public LLEditMenuHandler bool mFocusOnItemClicked; + /** Drag-to-reorder state */ + bool mAllowReorder; + bool mIsReordering; // a drag is currently in progress + bool mProcessingRightClick; // suppress drag arming during right-click handling + 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; + /** All pairs of the list */ pairs_list_t mItemPairs; diff --git a/indra/llui/llscrollcontainer.cpp b/indra/llui/llscrollcontainer.cpp index 17363719af..02b2fc144e 100644 --- a/indra/llui/llscrollcontainer.cpp +++ b/indra/llui/llscrollcontainer.cpp @@ -95,6 +95,9 @@ LLScrollContainer::LLScrollContainer(const LLScrollContainer::Params& p) mScrolledView(NULL), 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); @@ -197,11 +200,8 @@ void LLScrollContainer::reshape(S32 width, S32 height, bool show_h_scrollbar = false; calcVisibleSize( &visible_width, &visible_height, &show_h_scrollbar, &show_v_scrollbar ); - mScrollbar[VERTICAL]->setDocSize( scrolled_rect.getHeight() ); - mScrollbar[VERTICAL]->setPageSize( visible_height ); - - mScrollbar[HORIZONTAL]->setDocSize( scrolled_rect.getWidth() ); - mScrollbar[HORIZONTAL]->setPageSize( visible_width ); + preserveScrollbarMetrics(VERTICAL, show_v_scrollbar, scrolled_rect.getHeight(), visible_height); + preserveScrollbarMetrics(HORIZONTAL, show_h_scrollbar, scrolled_rect.getWidth(), visible_width); updateScroll(); } } @@ -586,6 +586,24 @@ bool LLScrollContainer::addChild(LLView* view, S32 tab_group) return ret_val; } +void LLScrollContainer::preserveScrollbarMetrics(EOrientation axis, bool show, S32 doc_size, S32 page_size) +{ + // Snapshot the position before the resize, but only while the scrollbar is + // visible: during a transient full-content pass it hides and its position is + // forced to 0, which would otherwise clobber the remembered position. + if (show && mScrollbar[axis]->getVisible()) + { + mStoredDocPos[axis] = mScrollbar[axis]->getDocPos(); + } + mScrollbar[axis]->setDocSize(doc_size); + mScrollbar[axis]->setPageSize(page_size); + if (show) + { + mScrollbar[axis]->setDocPos(mStoredDocPos[axis]); + mStoredDocPos[axis] = mScrollbar[axis]->getDocPos(); + } +} + void LLScrollContainer::updateScroll() { if (!getVisible() || !mScrolledView) @@ -605,6 +623,9 @@ void LLScrollContainer::updateScroll() calcVisibleSize( &visible_width, &visible_height, &show_h_scrollbar, &show_v_scrollbar ); S32 border_width = getBorderWidth(); + preserveScrollbarMetrics(VERTICAL, show_v_scrollbar, doc_height, visible_height); + preserveScrollbarMetrics(HORIZONTAL, show_h_scrollbar, doc_width, visible_width); + if( show_v_scrollbar ) { if( doc_rect.mTop < getRect().getHeight() - border_width ) @@ -667,12 +688,6 @@ void LLScrollContainer::updateScroll() mScrollbar[HORIZONTAL]->setVisible( false ); mScrollbar[HORIZONTAL]->setDocPos( 0 ); } - - mScrollbar[HORIZONTAL]->setDocSize( doc_width ); - mScrollbar[HORIZONTAL]->setPageSize( visible_width ); - - mScrollbar[VERTICAL]->setDocSize( doc_height ); - mScrollbar[VERTICAL]->setPageSize( visible_height ); } // end updateScroll void LLScrollContainer::setBorderVisible(bool b) diff --git a/indra/llui/llscrollcontainer.h b/indra/llui/llscrollcontainer.h index 20da32173d..c529678eb1 100644 --- a/indra/llui/llscrollcontainer.h +++ b/indra/llui/llscrollcontainer.h @@ -136,8 +136,10 @@ class LLScrollContainer : public LLUICtrl void updateScroll(); bool autoScroll(S32 x, S32 y, bool do_scroll); void calcVisibleSize( S32 *visible_width, S32 *visible_height, bool* show_h_scrollbar, bool* show_v_scrollbar ) const; + void preserveScrollbarMetrics(EOrientation axis, bool show, S32 doc_size, S32 page_size); LLScrollbar* mScrollbar[ORIENTATION_COUNT]; + S32 mStoredDocPos[ORIENTATION_COUNT]; S32 mSize; bool mIsOpaque; LLUIColor mBackgroundColor; diff --git a/indra/newview/llagentwearables.cpp b/indra/newview/llagentwearables.cpp index 8eb8754601..3ba1c74ec4 100644 --- a/indra/newview/llagentwearables.cpp +++ b/indra/newview/llagentwearables.cpp @@ -557,14 +557,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; @@ -1604,6 +1613,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 68c127c9ee..0387a40c07 100644 --- a/indra/newview/llagentwearables.h +++ b/indra/newview/llagentwearables.h @@ -139,6 +139,7 @@ class LLAgentWearables : public LLInitClass, public LLWearable 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 f602bfdcd9..0e88de70ab 100644 --- a/indra/newview/llappearancemgr.cpp +++ b/indra/newview/llappearancemgr.cpp @@ -4534,77 +4534,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 4f7b4c4047..31b10338e4 100644 --- a/indra/newview/llappearancemgr.h +++ b/indra/newview/llappearancemgr.h @@ -225,6 +225,14 @@ class LLAppearanceMgr: public LLSingleton 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 in a single batch. + bool reorderWearable(LLViewerInventoryItem* item, U32 new_index); + + // Apply a complete layer order for one wearable type in a single batch. + // 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 @@ -268,6 +276,10 @@ class LLAppearanceMgr: public LLSingleton 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..dac7bdc407 100644 --- a/indra/newview/llcofwearables.cpp +++ b/indra/newview/llcofwearables.cpp @@ -326,6 +326,9 @@ 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)); + //clothing is sorted according to its position relatively to the body mAttachments->setComparator(&WEARABLE_NAME_COMPARATOR); mBodyParts->setComparator(&WEARABLE_NAME_COMPARATOR); @@ -377,6 +380,41 @@ 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::onAccordionTabStateChanged(LLUICtrl* ctrl, const LLSD& expanded) { bool had_selected_items = mClothing->numSelected() || mAttachments->numSelected() || mBodyParts->numSelected(); @@ -560,6 +598,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..4013cbdb2d 100644 --- a/indra/newview/llcofwearables.h +++ b/indra/newview/llcofwearables.h @@ -102,6 +102,11 @@ class LLCOFWearables : public LLPanel 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); + LLPanelClothingListItem* buildClothingListItem(LLViewerInventoryItem* item, bool first, bool last); LLPanelBodyPartsListItem* buildBodypartListItem(LLViewerInventoryItem* item); LLPanelDeletableWearableListItem* buildAttachemntListItem(LLViewerInventoryItem* item); 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..ef2ffce30c 100644 --- a/indra/newview/skins/default/xui/en/panel_cof_wearables.xml +++ b/indra/newview/skins/default/xui/en/panel_cof_wearables.xml @@ -26,6 +26,7 @@ title="Clothing"> Toy Camera + Drag to reorder layers, or use the arrows Person (no name) Owner: