summaryrefslogtreecommitdiff
path: root/indra/newview
diff options
context:
space:
mode:
authorgwigz <gwigz@users.noreply.github.com>2026-06-25 20:08:15 +0100
committerGitHub <noreply@github.com>2026-06-25 22:08:15 +0300
commit9aaf3e7747dd92ecdbce9878d3ac19a0027845c4 (patch)
treec69d5b84444b323bfa5105e6a1c336a631632e95 /indra/newview
parent7d8e9d97c9f485ca413c98f4a543efe2bbd15bd9 (diff)
@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
Diffstat (limited to 'indra/newview')
-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
8 files changed, 213 insertions, 50 deletions
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 -->