commit 7c31167dbea9ffaaa2fd6e9f8d46f96ff207c105
parent 98daee6a460db2678bb95bcbddda008d866e3c7a
Author: Michael Camilleri <[email protected]>
Date: Fri, 13 Mar 2026 20:12:38 +0900
Support multi-selection with the mouse
This commit adds support for creating contiguous selections using the
mouse and Shift.
Co-Authored-By: Claude 4.6 Opus <[email protected]>
Diffstat:
3 files changed, 34 insertions(+), 11 deletions(-)
diff --git a/Listless/Extensions/TaskListView+Logic.swift b/Listless/Extensions/TaskListView+Logic.swift
@@ -271,8 +271,18 @@ extension TaskListViewProtocol {
deleteTask(task)
}
- func selectTask(_ taskID: UUID) {
- fState.selectedTaskID = taskID
+ func selectTask(_ taskID: UUID, extendSelection: Bool = false) {
+ if extendSelection && fState.selectedTaskID != nil {
+ if fState.anchorTaskID == nil {
+ fState.anchorTaskID = fState.cursorTaskID
+ }
+ fState.extendSelection(
+ to: taskID,
+ displayOrder: allTasksInDisplayOrder.map(\.id)
+ )
+ } else {
+ fState.selectedTaskID = taskID
+ }
}
func deleteTask(_ task: TaskItem) {
@@ -401,6 +411,9 @@ extension TaskListViewProtocol {
guard !ids.isEmpty else { return .handled }
let tasksToToggle = allTasksInDisplayOrder.filter { ids.contains($0.id) }
guard !tasksToToggle.isEmpty else { return .handled }
+ let hasActive = tasksToToggle.contains { !$0.isCompleted }
+ let hasCompleted = tasksToToggle.contains { $0.isCompleted }
+ guard !(hasActive && hasCompleted) else { return .handled }
for task in tasksToToggle {
toggleCompletion(task)
}
diff --git a/ListlessMac/Extensions/TaskListView+Toolbar.swift b/ListlessMac/Extensions/TaskListView+Toolbar.swift
@@ -33,15 +33,11 @@ extension TaskListView {
.help("Create a new item")
Button {
- if let currentID = fState.selectedTaskID,
- let task = allTasksInDisplayOrder.first(where: { $0.id == currentID })
- {
- deleteTask(task)
- }
+ _ = deleteSelectedTask()
} label: {
Label("Delete", systemImage: "trash")
}
- .disabled(fState.selectedTaskID == nil || focusedField != .scrollView)
+ .disabled(!canDeleteSelectionFromList)
.help("Delete selected item")
Divider()
diff --git a/ListlessMac/Views/TaskListView.swift b/ListlessMac/Views/TaskListView.swift
@@ -60,7 +60,11 @@ struct TaskListView: View, TaskListViewProtocol {
var canMarkSelectionCompleted: Bool {
guard focusedField == .scrollView else { return false }
- return allTasksInDisplayOrder.contains(where: { fState.isTaskSelected($0.id) })
+ let selected = allTasksInDisplayOrder.filter { fState.isTaskSelected($0.id) }
+ guard !selected.isEmpty else { return false }
+ let hasActive = selected.contains { !$0.isCompleted }
+ let hasCompleted = selected.contains { $0.isCompleted }
+ return !(hasActive && hasCompleted)
}
var markCompletedMenuTitle: String {
@@ -188,7 +192,12 @@ struct TaskListView: View, TaskListViewProtocol {
onToggle: { toggleCompletion($0) },
onTitleChange: { updateTitle($0, $1) },
onDelete: { deleteTask($0) },
- onSelect: { selectTask($0) },
+ onSelect: {
+ selectTask(
+ $0,
+ extendSelection: NSEvent.modifierFlags.contains(.shift)
+ )
+ },
onStartEdit: { startEditing($0) },
onEndEdit: { endEditing($0, shouldCreateNewTask: $1) },
onPaste: { createTask(title: $0, afterTaskID: taskID) }
@@ -330,7 +339,12 @@ struct TaskListView: View, TaskListViewProtocol {
onToggle: { toggleCompletion($0) },
onTitleChange: { updateTitle($0, $1) },
onDelete: { deleteTask($0) },
- onSelect: { selectTask($0) }
+ onSelect: {
+ selectTask(
+ $0,
+ extendSelection: NSEvent.modifierFlags.contains(.shift)
+ )
+ }
)
}
}