Select items by clicking the card instead of copying them

Copying was a side effect of a click, which made it impossible to build
a selection with the mouse and surprising when the intent was to pick
something up rather than copy it immediately. A click now selects (or
deselects), and copying is an explicit action from the selection bar or
the right-click menu.
This commit is contained in:
2026-10-09 17:58:38 +08:00
parent c7b4ed32f2
commit 6580a79d40
3 changed files with 32 additions and 29 deletions
+12 -11
View File
@@ -33,12 +33,11 @@ clicking an item copies it back to the clipboard.
Saves on every mutation. Saves on every mutation.
- `Sources/Prompt/PanelView.swift` — the panel UI. Header (section switcher, - `Sources/Prompt/PanelView.swift` — the panel UI. Header (section switcher,
search, add photos, close), item list, composer. Composer `# Name` creates/ search, add photos, close), item list, composer. Composer `# Name` creates/
switches sections. Click = copy; the row checkbox selects, Cmd+click toggles switches sections. Click = select (Shift+click for a range); the selection
multi-select, Shift+click selects a range, and a selection bar offers Copy as bar offers Copy (single item), Copy as List and Copy as Lines; right-click =
List / Copy as Lines; right-click = context menu acting on the selection context menu acting on the whole selection (Copy, Copy as List, Copy as
(Copy, Copy as List, Copy as Lines, Done, Edit, Open, Merge, Move To, Lines, Done, Edit, Open, Merge, Move To, Delete). Esc clears the selection
Delete). Esc clears the selection before it hides the before it hides the panel. Drop target for images/files.
panel. Drop target for images/files.
- `Sources/Prompt/PanelController.swift` — owns the floating `NSPanel` - `Sources/Prompt/PanelController.swift` — owns the floating `NSPanel`
(`.nonactivatingPanel`, `.floating`, right-edge docked, frame autosaved as (`.nonactivatingPanel`, `.floating`, right-edge docked, frame autosaved as
`PromptPanel`), the Cmd+V image-paste monitor, and `EditWindowController` `PromptPanel`), the Cmd+V image-paste monitor, and `EditWindowController`
@@ -172,11 +171,13 @@ brew upgrade --cask prompt
dispatches `paste:` to the responder chain itself for text, and adds the dispatches `paste:` to the responder chain itself for text, and adds the
clipboard image to the store when there is no text. `Prompt --diag` dumps the clipboard image to the store when there is no text. `Prompt --diag` dumps the
menu so the wiring can be checked without a keyboard. menu so the wiring can be checked without a keyboard.
- **A row is not a `Button`** because it contains a checkbox `Button` for - **A row is not a `Button`** because it contains a checkbox `Button`, and
selecting the item, and nested buttons never receive clicks. The row uses nested buttons never receive clicks. The row uses `contentShape` +
`contentShape` + `onTapGesture` for click-to-copy instead. Selection is `onTapGesture` so a click anywhere on the card selects it. Selection is
driven by the checkbox, Cmd+click (toggle) and Shift+click (range); the driven by the card click, the checkbox, Cmd+click (toggle) and Shift+click
context menu acts on the whole selection via `effectiveIDs(for:)`. (range); the context menu acts on the whole selection via `effectiveIDs(for:)`.
Copying is an explicit action (selection bar or context menu), never a side
effect of a click — that was changed deliberately.
- **Never bind bare keys as `.keyboardShortcut` in the panel** (Space, Delete, - **Never bind bare keys as `.keyboardShortcut` in the panel** (Space, Delete,
plain Cmd+C). SwiftUI key equivalents are matched before text input and plain Cmd+C). SwiftUI key equivalents are matched before text input and
would steal characters from the composer TextField. Only Cmd-modified would steal characters from the composer TextField. Only Cmd-modified
+9 -12
View File
@@ -2,25 +2,22 @@
A native macOS app for keeping prompts and photos, in the spirit of A native macOS app for keeping prompts and photos, in the spirit of
[Copper](https://shadcn.com/copper) by shadcn: a floating panel that sits next [Copper](https://shadcn.com/copper) by shadcn: a floating panel that sits next
to your work. Click anything to copy it back to your clipboard. to your work. Select what you need and send it back to your clipboard.
- **Checkbox to select** — every item has a checkbox; tick several and act on - **Click to select** — clicking a card selects it (Shift+click for a range),
them at once (copy, delete, move, merge). Clicking the row itself still and the selection bar copies a single item as-is, or the whole selection as a
copies that item. numbered list or as plain newline-separated lines.
- **Click to copy** — text notes go back as text, photos go back as images. - **Text or photo** — copying sends text back as text and a photo back as an
image.
- **Capture from anywhere** — select text in any app and tap **Left Shift twice**. - **Capture from anywhere** — select text in any app and tap **Left Shift twice**.
- **Photos** — paste (Cmd+V), drag & drop, or add via the photo button. - **Photos** — paste (Cmd+V), drag & drop, or add via the photo button.
- **Workspaces** — type `# Name` in the composer, or use "New Workspace" in - **Workspaces** — type `# Name` in the composer, or use "New Workspace" in
the Cmd+K switcher (which opens a name modal); Cmd+K switches, and the the Cmd+K switcher (which opens a name modal); Cmd+K switches, and the
up/down arrows in the switcher reorder them. (The code and the saved JSON up/down arrows in the switcher reorder them. (The code and the saved JSON
still call these `sections` so existing data keeps working.) still call these `sections` so existing data keeps working.)
- **Copy as List / Copy as Lines** — select with the checkboxes (or Cmd+click, - **Right-click for actions** — Copy, Copy as List, Copy as Lines, Mark as Done,
or Shift+click for a range), then copy the selection as a numbered list or as Edit, Open, Merge Notes, Move To, Delete — each acting on the whole selection.
plain newline-separated text, from the selection bar, the right-click menu, or - **Search** — Cmd+F searches across all workspaces.
Shift+Cmd+C / Option+Cmd+C. The same menu deletes, moves or merges the
selection.
- **Merge Notes, Mark as Done, Edit, Move To** — right-click any item.
- **Search** — Cmd+F searches across all sections.
- **Sync (optional)** — generate an 8-digit code (`1234-5678`) on one Mac and - **Sync (optional)** — generate an 8-digit code (`1234-5678`) on one Mac and
enter it on the other; no account, no login. The code is the only secret. enter it on the other; no account, no login. The code is the only secret.
- **Local & private** — everything lives in - **Local & private** — everything lives in
+11 -6
View File
@@ -242,7 +242,7 @@ struct PanelView: View {
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { handleClick(item) } .onTapGesture { handleClick(item) }
.help("Click to copy · checkbox to tick off · Cmd+click to select several · Shift+click for a range · right-click for more") .help("Click to select · Shift+click for a range · right-click to copy, delete and more")
.contextMenu { contextMenu(for: item) } .contextMenu { contextMenu(for: item) }
} }
@@ -561,6 +561,12 @@ struct PanelView: View {
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Spacer() Spacer()
if selection.count == 1, let id = selection.first, let item = store.item(id) {
Button("Copy") { copy(item) }
.font(.caption)
.buttonStyle(.plain)
.foregroundStyle(Color.accentColor)
}
Button("Copy as List") { copySelection(asList: true) } Button("Copy as List") { copySelection(asList: true) }
.font(.caption) .font(.caption)
.buttonStyle(.plain) .buttonStyle(.plain)
@@ -669,17 +675,16 @@ struct PanelView: View {
// MARK: - Actions // MARK: - Actions
/// A click anywhere on the card selects it (Shift+click selects a range).
/// Copying is then an action on the selection — from the selection bar or
/// the right-click menu — rather than the side effect of a click.
private func handleClick(_ item: PromptItem) { private func handleClick(_ item: PromptItem) {
let modifiers = NSApp.currentEvent?.modifierFlags ?? [] let modifiers = NSApp.currentEvent?.modifierFlags ?? []
if modifiers.contains(.command) {
toggleSelection(item.id)
return
}
if modifiers.contains(.shift), let anchor = lastSelectedID, anchor != item.id { if modifiers.contains(.shift), let anchor = lastSelectedID, anchor != item.id {
selectRange(from: anchor, to: item.id) selectRange(from: anchor, to: item.id)
return return
} }
copy(item) toggleSelection(item.id)
} }
private func toggleSelection(_ id: UUID) { private func toggleSelection(_ id: UUID) {