Selection.md (4891B)
1 # Findings: Cmd+Click Toggle for Multi-Select (Issue 1) 2 3 ## Current Implementation 4 5 The macOS selection model uses `FocusStateData` in 6 `Listless/Helpers/TaskListTypes.swift` with: 7 8 - `selectedTaskIDs: Set<UUID>` — the full set of selected task IDs 9 - `anchorTaskID: UUID?` — fixed end of a Shift+Arrow range 10 - `cursorTaskID: UUID?` — moving end of the range 11 - `selectedTaskID: UUID?` — convenience getter/setter that resets to 12 single-element selection 13 14 `extendSelection(to:displayOrder:)` computes a contiguous range from anchor to 15 cursor and replaces `selectedTaskIDs` entirely. This means the current model 16 has no concept of discontinuous selections. 17 18 Shift+Click is handled in `TaskListView+Logic.swift` via `selectTask(_:extendSelection:)`. 19 The macOS `TaskListView` body checks `NSEvent.modifierFlags.contains(.shift)` and 20 passes `extendSelection: true`. 21 22 Shift+Up/Down are `navigateUpExtend()`/`navigateDownExtend()` in `TaskListView+Logic.swift`. 23 They move the cursor one step and call `extendSelection(to:displayOrder:)`. 24 25 ## Proposed Cmd+Click Behaviour 26 27 Cmd+Click should toggle individual items in and out of a multi-selection. 28 29 ### State Model 30 31 After a Cmd+Click, the state consists of: 32 33 - **anchor** — set to the item immediately below the Cmd+Clicked item 34 - **cursor** — reset to the same position as anchor 35 - **inactive selections** — all other selected items outside the anchor-cursor 36 range (these are carried forward from the previous selection) 37 38 The visible selection is: `inactive ∪ range(anchor, cursor)`. 39 40 ### Shift+Arrow After Cmd+Click 41 42 Shift+Up/Down moves the cursor. The selection is recomputed as 43 `inactive ∪ range(anchor, cursor)`. The range flips direction when the cursor 44 crosses the anchor. 45 46 **Merge rule**: when the active range becomes adjacent to (or overlaps with) an 47 inactive range, the inactive items are absorbed into the active range and the 48 cursor jumps to the far end of the merged region (away from the anchor). After 49 a merge, the selection is fully contiguous and subsequent Shift+Arrow presses 50 behave as normal anchored selection. 51 52 ### Worked Example 53 54 Starting list: A, B, C, D, E, F, G, H, I 55 56 | Step | Action | Cursor | Anchor | Active Range | Inactive | Selection | 57 |------|----------------|--------|--------|--------------|----------|-------------| 58 | 1 | Select D-G | G | D | {D,E,F,G} | {} | {D,E,F,G} | 59 | 2 | Cmd+Click E | F | F | {F} | {D,G} | {D,F,G} | 60 | 3 | Shift+Up | E | F | {E,F} | {D,G} | {D,E,F,G} | 61 62 Wait — this doesn't match. The user's test showed step 2 → step 3 as 63 {D,F,G} → {D,F} with Shift+Up. That means the cursor after Cmd+Click was 64 at G (the previous cursor), not reset to the anchor. 65 66 **Corrected model**: after Cmd+Click, the cursor stays at its previous position 67 (G), and the anchor moves to the item below the clicked item (F). 68 69 | Step | Action | Cursor | Anchor | Active Range | Inactive | Selection | 70 |------|----------------|--------|--------|--------------|----------|---------------| 71 | 1 | Select D-G | G | D | {D,E,F,G} | {} | {D,E,F,G} | 72 | 2 | Cmd+Click E | G | F | {F,G} | {D} | {D,F,G} | 73 | 3 | Shift+Up | F | F | {F} | {D} | {D,F} | 74 | 4 | Shift+Down | G | F | {F,G} | {D} | {D,F,G} | 75 | 5 | Shift+Up | F | F | {F} | {D} | {D,F} | 76 | 6 | Shift+Up | E | F | {E,F} | {D}* | {D,E,F} | 77 | 7 | Shift+Up | C | F | {C,D,E,F} | {} | {C,D,E,F} | 78 | 8 | Shift+Down | D | F | {D,E,F} | {} | {D,E,F} | 79 | 9 | Shift+Down | E | F | {E,F} | {} | {E,F} | 80 | 10 | Shift+Down | F | F | {F} | {} | {F} | 81 82 *At step 6, the active range {E,F} becomes adjacent to inactive {D}. Merge 83 occurs: inactive is cleared and the cursor jumps from E to D (the far end of 84 the merged region, away from the anchor). This is why step 7's Shift+Up moves 85 the cursor from D to C, not from E to D. 86 87 ### Edge Cases Still to Decide 88 89 - **Cmd+Click on the bottom-most item**: there is no item below to anchor to. 90 Options: anchor to the clicked item itself, or anchor to the item above. 91 - **Multiple Cmd+Clicks**: each Cmd+Click sets the anchor to the item below the 92 clicked item; cursor stays at its previous position. The inactive set is 93 whatever is selected outside the active range. (This is a simplification; 94 actual Finder behaviour in macOS 15 varies depending on whether you deselect 95 going up vs down the list.) 96 - **Cmd+Click to add** (clicking an unselected item): needs testing. Presumably 97 the item is added to the selection and anchor/cursor update the same way.