← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


Docs/Selection.md (4891B)

1# Findings: Cmd+Click Toggle for Multi-Select (Issue 1)
2
3## Current Implementation
4
5The 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
15cursor and replaces `selectedTaskIDs` entirely. This means the current model
16has no concept of discontinuous selections.
17
18Shift+Click is handled in `TaskListView+Logic.swift` via `selectTask(_:extendSelection:)`.
19The macOS `TaskListView` body checks `NSEvent.modifierFlags.contains(.shift)` and
20passes `extendSelection: true`.
21
22Shift+Up/Down are `navigateUpExtend()`/`navigateDownExtend()` in `TaskListView+Logic.swift`.
23They move the cursor one step and call `extendSelection(to:displayOrder:)`.
24
25## Proposed Cmd+Click Behaviour
26
27Cmd+Click should toggle individual items in and out of a multi-selection.
28
29### State Model
30
31After 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
38The visible selection is: `inactive ∪ range(anchor, cursor)`.
39
40### Shift+Arrow After Cmd+Click
41
42Shift+Up/Down moves the cursor. The selection is recomputed as
43`inactive ∪ range(anchor, cursor)`. The range flips direction when the cursor
44crosses the anchor.
45
46**Merge rule**: when the active range becomes adjacent to (or overlaps with) an
47inactive range, the inactive items are absorbed into the active range and the
48cursor jumps to the far end of the merged region (away from the anchor). After
49a merge, the selection is fully contiguous and subsequent Shift+Arrow presses
50behave as normal anchored selection.
51
52### Worked Example
53
54Starting 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
62Wait — 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
64at 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
83occurs: inactive is cleared and the cursor jumps from E to D (the far end of
84the merged region, away from the anchor). This is why step 7's Shift+Up moves
85the 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.