Fix menu click sometimes pasting the newest clipping instead of the clicked one#7
Open
MiMoHo wants to merge 2 commits into
Open
Fix menu click sometimes pasting the newest clipping instead of the clicked one#7MiMoHo wants to merge 2 commits into
MiMoHo wants to merge 2 commits into
Conversation
…licked one The pollPB: timer runs in NSRunLoopCommonModes, so a clipboard change can be noticed while the status menu is open (deliberate, for Universal Clipboard). That triggers updateMenu, which removes every clipping item and inserts new NSMenuItem objects. If the rebuild lands between the user's click and the action dispatch, the clicked item is no longer in the menu, [sender menu] is nil, and [[sender menu] indexOfItem:sender] messages nil and yields 0 -- pasting stack position 0, the most recently copied clipping, instead of the entry the user clicked. With the 1-second poll interval this commonly happens when the user copies something and opens the menu right away. - processMenuClippingSelection: bail out when the sender is orphaned or its index can't be resolved, instead of pasting the wrong clipping. - pasteIndexAndUpdate: now returns whether a clipping was placed on the pasteboard, so no Cmd-V is faked when nothing was pasted (previously that re-pasted whatever was already on the pasteboard). Also bounds-check the search mapping, which could throw NSRangeException. - searchWindowItemSelected: use clickedRow for double-clicks so a selection change between click and action can't redirect the paste to row 0, and bounds-check the search mapping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pollPB: runs in NSRunLoopCommonModes so it keeps firing during menu tracking. When it notices a clipboard change mid-selection it inserts a new clipping at store index 0, shifting every clipping's index by one. The pending selection is then resolved against the shifted store, so the user pastes the wrong entry - typically the freshly-arrived newest one - instead of the one they clicked. The same shift breaks the search window, whose result list is a snapshot while getPasteFromIndex: uses live indices. Guard pollPB: to skip capture while isMenuOpen or isSearchWindowDisplayed, without touching pbCount so the change is still detected and captured the moment the surface closes (menuDidClose: fires a catch-up poll). Track menu open state via menuWillOpen:/menuDidClose:. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
Sometimes, clicking an older entry in the status menu pastes the most recently copied clipping instead of the one that was clicked. It is intermittent and more likely the further down the list you click.
Root cause
The chain is fully deterministic once the timing lines up:
pollPB:runs on a 1-second timer scheduled inNSRunLoopCommonModes— deliberately, so Universal Clipboard arrivals are noticed while the menu is open (comment inawakeFromNib).When it notices a clipboard change,
addClipping:firesupdateMenu→updateMenuContaining:, which removes every clipping item and inserts brand-newNSMenuItemobjects (also scheduled inNSRunLoopCommonModes).NSMenu dispatches the clicked item's action slightly after mouse-up (the highlight blink). If the rebuild from step 2 lands in that window, the clicked item has been removed from the menu, so in
processMenuClippingSelection::[sender menu]isnil, messaging nil yields0, andpasteIndexAndUpdate:0pastes stack position 0 — the newest clipping. WithmenuSelectionPastesdefaulting to YES, the fake Cmd-V then pastes it into the frontmost app.No external clipboard source is needed: with the 1 s poll interval, the user's own Cmd-C is often detected only after the menu is already open (copy → open menu → browse → click lands right around the poll tick).
Standalone proof of the mechanism (AppKit, compiled with
clang -fno-objc-arc -framework AppKit):(Test source is a ~50-line program that builds a menu, retains item 7 as
sender, replaces all items the wayupdateMenuContaining:does, and evaluates the exact expression fromprocessMenuClippingSelection:.)Fix
This PR now contains two commits — defensive guards at the point of paste, and root-cause prevention of the mid-selection rebuild.
Commit 1 — defensive guards
processMenuClippingSelection:— resolve the index defensively; if the clicked item is orphaned (menunil) or its index can't be resolved, do nothing rather than paste the wrong clipping. The menu is current again on the next click.pasteIndexAndUpdate:— now returns whether a clipping was actually placed on the pasteboard. Previously, when it silently found no content, the caller still faked Cmd-V, re-pasting whatever was already on the pasteboard (same visible symptom through a second hole). Also bounds-checks the search mapping, which could throwNSRangeExceptionwhen the list changed between menu build and click.searchWindowItemSelected:— for double-clicks, useclickedRowinstead ofselectedRow, so a selection reset between click and action (e.g.updateSearchResultsre-selecting row 0 when the search field action fires) can't redirect the paste to the newest entry; double-clicks below the last row are ignored; the search mapping is bounds-checked.Commit 2 — freeze capture while the menu/search window is open
This is the follow-up flagged as out-of-scope in the original writeup, now included. It attacks the root cause instead of only softening the symptom: a rebuild landing during menu tracking also shifts items under the cursor, so a click can hit the visual neighbour of the intended entry (and the store index the click resolves to no longer matches what the user saw).
BOOL isMenuOpen, set inmenuWillOpen:/ cleared inmenuDidClose:.pollPB:returns early whileisMenuOpen || isSearchWindowDisplayed, without touchingpbCount, so the clipboard change is still detected and captured the moment the surface closes;menuDidClose:runs a catch-uppollPB:for immediacy.With capture frozen, the store cannot mutate between menu-open and click, so the item indices the user sees stay valid — the click resolves to the entry they actually clicked (Commit 1's guards then remain as belt-and-suspenders for any residual orphaning). Trade-off: a clipping that arrives while the menu is open is picked up on close rather than appearing live in the open menu — a fair price for never pasting the wrong entry, and it also stops items jumping under the cursor mid-selection.
Verification
xcodebuild -scheme Flycut -configuration Release,BUILD SUCCEEDED) — both commits together. The original standalone mechanism test above still stands for the nil-menu path.clang -fsyntax-onlydiagnostics matchmaster(no new warnings).🤖 Generated with Claude Code