Skip to content

feat(ui): restore contiguous shift-click range selection in the cue list #790

Description

@one-soft-engineer

Summary

Since #786, -click on a cue row toggles that single row's membership of the
selection, exactly like -click. Contiguous range selection ("click row 3,
shift-click row 9, get rows 3 through 9") is no longer reachable anywhere in the
cue list.

Background

Before #786, the cue list relied on List's native multi-selection, which gave
-click range selection for free. #786 made a plain click inside the #,
Name, or Info column open that column's TextField, so the row's own tap
handlers now claim the click before List sees it, and the native behaviour is
unreachable.

CueRowTap.intent (OnlyCue/UI/CueRowTapIntent.swift:41) collapses both
modifiers into one isExtending: Bool:

case .stripe:
    return isExtending ? .extendSelection : .selectAndSeek

and CueListPane implements .extendSelection as a symmetric difference —
a toggle:

onExtendSelection: {
    var updated = selection
    updated.formSymmetricDifference([cue.id])
    lastRowTapSelection = updated
    selection = updated
},

This was a conscious trade-off in #786 ( and deliberately behaved alike
to keep the truth table at eight rows), not an oversight, but it is a loss of
function on a list that routinely runs to hundreds of cues.

Proposed change

Split the two modifiers:

  • -click keeps today's toggle behaviour.
  • -click selects the inclusive range between the anchor and the clicked
    row, replacing the current selection.

The anchor is the last row selected without (a plain stripe click, a
-click, or a click that began an edit). A -click does not move the
anchor, so repeated -clicks re-range from the same origin — matching Finder
and every spreadsheet.

This means CueRowTapIntent gains a case (extendRange) and
CueRowTap.intent gains a parameter distinguishing from . Ordering
comes from the pane's filtered, sorted cue array, not from Cue.id — the
range must follow what the user sees.

Acceptance criteria

  • Given rows 1-10, when the user clicks row 3's stripe then -clicks row
    9, then rows 3 through 9 inclusive are selected.
  • Given the same, when the user then -clicks row 5, then rows 3 through
    5 are selected (the anchor stayed at 3, it did not move to 9).
  • Given a -click range, when the user -clicks a row inside it, then
    only that row is removed and the rest of the range stays selected.
  • -clicking backwards (anchor below the target) selects the same
    inclusive range.
  • With a filter active, a -click range covers only rows visible under
    the filter, in displayed order.
  • -click never opens a TextField and never moves the playhead.
  • In Show mode the columns stay locked and -click on the stripe still
    ranges.
  • Unit tests cover the extended CueRowTap.intent truth table; a UI test
    in CueListSingleClickEditUITests covers the range-select path.

Files

  • OnlyCue/UI/CueRowTapIntent.swift — add the case and its parameter.
  • OnlyCue/UI/CueRowView.swift — read NSEvent.modifierFlags for .shift
    separately from .command.
  • OnlyCue/UI/CueListPane.swift — hold the anchor, resolve the range against
    the displayed cue order, keep lastRowTapSelection in sync so the
    reveal-the-row onChange still skips self-inflicted changes.
  • OnlyCueTests/CueRowTapIntentTests.swift, OnlyCueUITests/CueListSingleClickEditUITests.swift

Spec

docs/superpowers/specs/2026-08-28-cue-list-single-click-edit.md — modifier-click
multi-select. docs/ui-sections.md "Cue Row Interaction" needs its /
sentence corrected once this lands.

Follows #786. Related: #787.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:uiSwiftUI viewsenhancementNew feature or requestp2Nice to havetype:featUser-visible feature

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions