Skip to content

Port clothing layer drag-and-drop refinements#329

Open
gwigz wants to merge 1 commit into
AlchemyViewer:developfrom
gwigz:gwigz/cof-dnd-updates
Open

Port clothing layer drag-and-drop refinements#329
gwigz wants to merge 1 commit into
AlchemyViewer:developfrom
gwigz:gwigz/cof-dnd-updates

Conversation

@gwigz

@gwigz gwigz commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Brings the COF clothing layer reorder feature up to the final upstream
state (secondlife/viewer#5918 + secondlife/viewer#5959), on top of the
two commits already cherry-picked here:

- LLScrollContainer: replace the bespoke preserveScrollbarMetrics helper
  with a keep_scroll_pos param, so the clothing list no longer jumps its
  scroll position on rebuild
- LLFlatListView: arm reorder drag in inter-row gaps (handleMouseDown +
  getReorderPairAt), cancel an active drag on visibility change
  (alt-tab), replace the mProcessingRightClick hack with EMouseClickType,
  add a reorder-ended callback fired on cancelled drags, report the moved
  row's visible index, plus the secondlife/viewer#5959
  nullptr/constexpr/formatting cleanup
- LLCOFWearables: defer a refresh that arrives mid-drag and re-run it when
  the drag ends, so a COF change can't cancel an active reorder
- panel_cof_wearables.xml: keep_scroll_pos="true" on the clothing list

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Clothing lists now preserve their scroll position while you reorganize items.
    • Right-clicks and left-clicks on list items are handled separately for better interaction.
  • Bug Fixes

    • Drag-and-drop reordering is more reliable, including row selection and drop placement.
    • Clothing refreshes now wait until a reorder is finished, preventing list updates from interrupting your drag.

Walkthrough

LLFlatListView now distinguishes left and right clicks, maps padding presses to reorder rows, and reports reorder completion. LLScrollContainer can preserve scroll position, and LLCOFWearables defers refreshes during active clothing reorders.

Changes

Clothing list interaction updates

Layer / File(s) Summary
Mouse routing and padding hit-testing
indra/llui/llflatlistview.h, indra/llui/llflatlistview.cpp
LLFlatListView threads EMouseClickType through item clicks, routes right clicks separately, and maps padding presses to the nearest reorder row.
Reorder drag lifecycle and callbacks
indra/llui/llflatlistview.h, indra/llui/llflatlistview.cpp
Reorder state cleanup, insert-boundary selection, indicator drawing, and the reorder-ended callback path are updated to skip invisible or dragged rows and report a recomputed visible index.
Scroll position preservation
indra/llui/llscrollcontainer.h, indra/llui/llscrollcontainer.cpp, indra/newview/skins/default/xui/en/panel_cof_wearables.xml
LLScrollContainer adds keep_scroll_pos, stores it on construction, and uses it in reshape/update logic; the Clothing tab enables the new setting.
Deferred COF refresh after reorder
indra/newview/llcofwearables.h, indra/newview/llcofwearables.cpp, indra/newview/llappearancemgr.h
LLCOFWearables defers refresh() while clothing reorder is active, registers onReorderEnded(), and reruns the refresh when the drag ends; the reorderWearable comment is updated.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant LLFlatListView
  participant LLCOFWearables

  User->>LLFlatListView: drag clothing row
  LLCOFWearables->>LLFlatListView: isReorderActive()
  LLFlatListView-->>LLCOFWearables: true
  LLCOFWearables->>LLCOFWearables: set mRefreshPending
  User->>LLFlatListView: finish drag
  LLFlatListView->>LLCOFWearables: onReorderEnded()
  LLCOFWearables->>LLCOFWearables: refresh()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • AlchemyViewer/Alchemy#317: This PR adds the LLFlatListView reorder flow and clothing callback wiring that this change extends.

Poem

I twitched my nose and danced around,
With scrolls that stay and rows that found.
Left paw, right paw, reorder neat,
The callback drums a bunny beat.
Now every carrot list feels sweet. 🐰

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete and does not follow the template's required sections or provide an issue link. Add a clear Description, a linked Related Issues entry, and fill the checklist and additional notes sections as needed.
Docstring Coverage ⚠️ Warning Docstring coverage is 9.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the PR's clothing layer drag-and-drop refinements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
indra/llui/llflatlistview.cpp (1)

965-983: 🩺 Stability & Availability | 🔴 Critical

Critical Double-Free in removeItemPair due to Callback Re-entrancy

clearReorderDragState() invokes mReorderEndedCallback() before removeItemPair() has erased or deleted the target item. In LLCOFWearables, this callback triggers refresh() -> clear(), which iterates mItemPairs and calls removeItemPair() for every item, including the one currently being processed.

Consequence:

  1. clear() calls removeItemPair() recursively for the active item.
  2. The recursive call erases the item from mItemPairs and executes delete item_pair.
  3. Control returns to the original removeItemPair(), which then executes delete item_pair again on the same memory address.
    This results in a guaranteed crash (double-free) or heap corruption.

Fix Required:
Move mReorderEndedCallback() invocation to after removeItemPair() returns, or guard refresh()/clear() to prevent list mutation while a removal operation is active.

[Lines 1567-1590, 974-980, 434-470]

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@indra/llui/llflatlistview.cpp` around lines 965 - 983, The reorder-end
callback is being fired from clearReorderDragState() before the active item
removal finishes, which lets LLCOFWearables::refresh() re-enter
clear()/removeItemPair() and double-delete the same item. Update
LLFlatListView::removeItemPair() / clearReorderDragState() so
mReorderEndedCallback runs only after the removal path has fully completed, or
add a re-entrancy guard around clear()/refresh() while a removal is in progress.
Use the symbols clearReorderDragState(), removeItemPair(), and
mReorderEndedCallback to keep the fix localized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@indra/llui/llscrollcontainer.cpp`:
- Around line 698-708: The restored document positions are only being applied to
the scrollbar state in the mKeepScrollPos branch, so mScrolledView can still
render at the old or clamped location until the next updateScroll() pass. Update
the LLScrollContainer flow so the restored positions are applied immediately to
the scrolled view after scrollVertical() and scrollHorizontal() run, using the
existing mStoredDocPos values and the related scrollbar/doc-position logic in
LLScrollContainer rather than waiting for a later refresh.

---

Outside diff comments:
In `@indra/llui/llflatlistview.cpp`:
- Around line 965-983: The reorder-end callback is being fired from
clearReorderDragState() before the active item removal finishes, which lets
LLCOFWearables::refresh() re-enter clear()/removeItemPair() and double-delete
the same item. Update LLFlatListView::removeItemPair() / clearReorderDragState()
so mReorderEndedCallback runs only after the removal path has fully completed,
or add a re-entrancy guard around clear()/refresh() while a removal is in
progress. Use the symbols clearReorderDragState(), removeItemPair(), and
mReorderEndedCallback to keep the fix localized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cdaed76b-fdaf-4c45-98f2-6e461d9aa829

📥 Commits

Reviewing files that changed from the base of the PR and between d2546ef and db325e0.

📒 Files selected for processing (8)
  • indra/llui/llflatlistview.cpp
  • indra/llui/llflatlistview.h
  • indra/llui/llscrollcontainer.cpp
  • indra/llui/llscrollcontainer.h
  • indra/newview/llappearancemgr.h
  • indra/newview/llcofwearables.cpp
  • indra/newview/llcofwearables.h
  • indra/newview/skins/default/xui/en/panel_cof_wearables.xml

Comment thread indra/llui/llscrollcontainer.cpp
@gwigz

gwigz commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai resolve

clear() doesn't call removeItemPair()

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="AlchemyViewer/Alchemy"
pr_number="329"

echo "== Review comments by coderabbitai on PR #$pr_number =="
gh api "repos/$repo/pulls/$pr_number/comments?per_page=100" --paginate \
  --jq '.[] | select(.user.login=="coderabbitai") | {id, node_id, path, line, body, url}'

echo
echo "== Matching comments mentioning removeItemPair or clear() =="
gh api "repos/$repo/pulls/$pr_number/comments?per_page=100" --paginate \
  --jq '.[] | select(.user.login=="coderabbitai") | select((.body | test("removeItemPair|clear\\(\\)"; "i"))) | {id, node_id, path, line, body, url}'

echo
echo "== Check relevant file outline =="
fd -i 'llflatlistview.cpp' . -x sh -c 'echo "--- $1"; ast-grep outline "$1" --view expanded | sed -n "1,220p"' sh {}

echo
echo "== Search for clear() and removeItemPair() definitions/usages in LLFlatListView =="
fd -i 'llflatlistview.cpp' . -x sh -c '
  file="$1"
  echo "--- $file"
  rg -n -C3 "\\b(clear|removeItemPair)\\s*\\(" "$file"
' sh {}

Length of output: 5037


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="AlchemyViewer/Alchemy"
owner="${repo%%/*}"
name="${repo#*/}"
pr_number=329

echo "== PR review threads with comments by coderabbitai =="
gh api graphql \
  -F owner="$owner" \
  -F name="$name" \
  -F pr="$pr_number" \
  -f query='
query($owner: String!, $name: String!, $pr: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $pr) {
      reviewThreads(first: 100) {
        nodes {
          id
          isResolved
          isOutdated
          path
          line
          comments(first: 20) {
            nodes {
              id
              databaseId
              url
              body
              author { login }
            }
          }
        }
      }
    }
  }
}' --jq '
.data.repository.pullRequest.reviewThreads.nodes[]
| select(any(.comments.nodes[]; .author.login=="coderabbitai"))
| {
    thread_id: .id,
    isResolved,
    isOutdated,
    path,
    line,
    comments: [.comments.nodes[] | {author: .author.login, databaseId, url, body}]
  }'

echo
echo "== Top-level PR issue comments mentioning removeItemPair or clear() =="
gh api "repos/$repo/issues/$pr_number/comments?per_page=100" --paginate \
  --jq '.[] | select((.body | test("removeItemPair|clear\\(\\)|resolve"; "i"))) | {id, user: .user.login, body, url}'

Length of output: 7021



@gwigz Thanks — that confirms it. I don’t see any open CodeRabbit review thread for this specific concern to resolve; the only review thread returned is already resolved and unrelated. Given the code in indra/llui/llflatlistview.cpp, this concern should be treated as withdrawn.


✏️ Learnings added
Learnt from: gwigz
URL: https://github.com/AlchemyViewer/Alchemy/pull/329

Timestamp: 2026-06-26T18:54:50.560Z
Learning: In `indra/llui/llflatlistview.cpp` in the AlchemyViewer/Alchemy codebase, `LLFlatListView::clear()` directly tears down and deletes `mItemPairs` instead of routing through `LLFlatListView::removeItemPair(item_pair_t*, bool)`. Future reviews should not assume `clear()` uses `removeItemPair()` as its teardown path.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant