Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- Restored the task search/filter bar in the popover, lost in the AppKit rewrite: real-time title/notes filtering, result count, matching subtasks shown with their parents, and an auto-expanded Completed section during search.
- Added Homebrew tap installation instructions and release maintenance notes.
- Added an opt-in testing window UI mode for local AppKit interaction outside the menu bar popover.
- The mouse cursor now turns into a pointing hand over the task completion circle, in both the task list and the task edit screen.
- Added a right-click Delete action for task rows in the popover task list.
- Added seeded task data for testing-window verification.
- Added a Long Subtasks testing-window list with a parent task containing 12 subtasks, including several completed subtasks.
Expand Down
30 changes: 17 additions & 13 deletions TaskMenu.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
C1EA9BDD5C59238997F833F1 /* TestingWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9141316E1D908A6C099CF691 /* TestingWindowController.swift */; };
C3C41DC18427B14CEA020FD6 /* SettingsLaunchAtLoginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF81FBE51E0B1CC80B1A5FD /* SettingsLaunchAtLoginTests.swift */; };
C4D74AEE4D4E0555EA0336A2 /* GitHubUpdateChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FC8DA7EF5FEF032B81469A3 /* GitHubUpdateChecker.swift */; };
CE62B34C489FAB410B59EF62 /* TaskMenuActionButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33B8E5D270E820D7C6070D4D /* TaskMenuActionButtonTests.swift */; };
D321CE95DA3DC0D6090CD619 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB598B8D34C0A014D7826A63 /* MockURLProtocol.swift */; };
D75722B06B77536D032C2FCD /* DueDateNotificationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE4010667E3A28546894589 /* DueDateNotificationServiceTests.swift */; };
DE51E84794E46D07C1401DE6 /* TaskList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 633899C51990EF71D24F5C89 /* TaskList.swift */; };
Expand Down Expand Up @@ -78,6 +79,7 @@
246C75CF9B4CE92C49A9A967 /* TaskListContentAppKitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListContentAppKitView.swift; sourceTree = "<group>"; };
2667803C667BBD21DCCCD029 /* TaskListAppKitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListAppKitViewController.swift; sourceTree = "<group>"; };
2C84C4C64E2BF982920144CA /* TaskListControlsAppKitViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListControlsAppKitViews.swift; sourceTree = "<group>"; };
33B8E5D270E820D7C6070D4D /* TaskMenuActionButtonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskMenuActionButtonTests.swift; sourceTree = "<group>"; };
3F2D65032E1F2A015F67B6D0 /* TaskPopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskPopoverViewController.swift; sourceTree = "<group>"; };
3FC8DA7EF5FEF032B81469A3 /* GitHubUpdateChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubUpdateChecker.swift; sourceTree = "<group>"; };
4041A68FD32907E3B89B8F8B /* MenuBarWindowGlassSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarWindowGlassSupport.swift; sourceTree = "<group>"; };
Expand All @@ -90,11 +92,11 @@
62C9B2D2EE1B7668B9245DBE /* TaskDetailViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskDetailViewTests.swift; sourceTree = "<group>"; };
633899C51990EF71D24F5C89 /* TaskList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskList.swift; sourceTree = "<group>"; };
63E868E0B0520F6ABD40D350 /* GoogleAuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthService.swift; sourceTree = "<group>"; };
66948CA865C6CDB97011D782 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
68060F2FA94B23E37273F003 /* MenuBarWindowChromeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarWindowChromeTests.swift; sourceTree = "<group>"; };
68CD93FC69779BFB08340F08 /* TaskMenu.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TaskMenu.entitlements; sourceTree = "<group>"; };
6E4CD6808B2587AD2F8F9563 /* TaskItemModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItemModelTests.swift; sourceTree = "<group>"; };
77DBAAB7CC102C6006686981 /* MetricKitPayloadStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricKitPayloadStoreTests.swift; sourceTree = "<group>"; };
7B9B96C944BC0E6F55E3D791 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
7DF81FBE51E0B1CC80B1A5FD /* SettingsLaunchAtLoginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLaunchAtLoginTests.swift; sourceTree = "<group>"; };
85062E7D51749F3ED4FD688D /* AppKitTaskUIHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppKitTaskUIHelpers.swift; sourceTree = "<group>"; };
8C8C52C19BDA6F8F2D3C638F /* AppIcon.svg */ = {isa = PBXFileReference; path = AppIcon.svg; sourceTree = "<group>"; };
Expand Down Expand Up @@ -122,15 +124,6 @@
/* End PBXFileReference section */

/* Begin PBXGroup section */
02C7954B6F0AE7D4B33301DC /* TaskMenu */ = {
isa = PBXGroup;
children = (
7B9B96C944BC0E6F55E3D791 /* Config.xcconfig */,
);
name = TaskMenu;
path = .;
sourceTree = "<group>";
};
141A484087CC4923B5F2371C /* Resources */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -176,6 +169,15 @@
path = Models;
sourceTree = "<group>";
};
82BE47CB8A813E4BEDC55BD9 /* pointing-hand-cursor */ = {
isa = PBXGroup;
children = (
66948CA865C6CDB97011D782 /* Config.xcconfig */,
);
name = "pointing-hand-cursor";
path = .;
sourceTree = "<group>";
};
A34BA795C75683448AFABACA /* TaskMenu */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -237,6 +239,7 @@
62C9B2D2EE1B7668B9245DBE /* TaskDetailViewTests.swift */,
6E4CD6808B2587AD2F8F9563 /* TaskItemModelTests.swift */,
A2191A69341B04C760925B55 /* TaskListViewTests.swift */,
33B8E5D270E820D7C6070D4D /* TaskMenuActionButtonTests.swift */,
9C0DB3189CC6045A5C0A230F /* TaskMenuAppTests.swift */,
ADED99C71B9C470D7D37974C /* TestDueDateNotificationService.swift */,
);
Expand All @@ -246,8 +249,8 @@
E7776CF5C61A2557700CB1FE = {
isa = PBXGroup;
children = (
82BE47CB8A813E4BEDC55BD9 /* pointing-hand-cursor */,
A34BA795C75683448AFABACA /* TaskMenu */,
02C7954B6F0AE7D4B33301DC /* TaskMenu */,
C53DB1319E1111ADB350C841 /* TaskMenuTests */,
1E011DB940E268BD3223C95C /* Products */,
);
Expand Down Expand Up @@ -397,6 +400,7 @@
F583510B7875405F768BADD3 /* TaskDetailViewTests.swift in Sources */,
0FD718B29CF8183561A6F550 /* TaskItemModelTests.swift in Sources */,
B1E9E90C4184BDD3DED97A2B /* TaskListViewTests.swift in Sources */,
CE62B34C489FAB410B59EF62 /* TaskMenuActionButtonTests.swift in Sources */,
44D8CA5D6D124CF97A57EF1D /* TaskMenuAppTests.swift in Sources */,
44A6F4A2E806FF1D44F0E19B /* TestDueDateNotificationService.swift in Sources */,
);
Expand Down Expand Up @@ -436,7 +440,7 @@
};
6EF47391B5EF770AC931DC3F /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7B9B96C944BC0E6F55E3D791 /* Config.xcconfig */;
baseConfigurationReference = 66948CA865C6CDB97011D782 /* Config.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
Expand Down Expand Up @@ -524,7 +528,7 @@
};
A734F39AA9B691088E02AEFF /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7B9B96C944BC0E6F55E3D791 /* Config.xcconfig */;
baseConfigurationReference = 66948CA865C6CDB97011D782 /* Config.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
Expand Down
26 changes: 24 additions & 2 deletions TaskMenu/Views/AppKitTaskUIHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ final class TaskMenuAppStateObserver {
final class TaskMenuActionButton: NSButton {
var onPress: (() -> Void)?
var onHoverChanged: ((Bool) -> Void)?
/// Shows the pointing-hand cursor while the mouse is over the button.
var usesPointingHandCursor = false {
didSet {
guard usesPointingHandCursor != oldValue else { return }
updateTrackingAreas()
}
}
private var hoverTrackingArea: NSTrackingArea?

init(
Expand Down Expand Up @@ -160,24 +167,39 @@ final class TaskMenuActionButton: NSButton {
removeTrackingArea(hoverTrackingArea)
self.hoverTrackingArea = nil
}
guard onHoverChanged != nil else { return }
guard onHoverChanged != nil || usesPointingHandCursor else { return }
var options: NSTrackingArea.Options = [.activeAlways, .mouseEnteredAndExited, .inVisibleRect]
if usesPointingHandCursor {
options.insert(.cursorUpdate)
}
let area = NSTrackingArea(
rect: bounds,
options: [.activeAlways, .mouseEnteredAndExited, .inVisibleRect],
options: options,
owner: self,
userInfo: nil
)
hoverTrackingArea = area
addTrackingArea(area)
}

override func cursorUpdate(with event: NSEvent) {
guard usesPointingHandCursor, isEnabled else {
super.cursorUpdate(with: event)
return
}
NSCursor.pointingHand.set()
}

override func mouseEntered(with event: NSEvent) {
super.mouseEntered(with: event)
onHoverChanged?(true)
}

override func mouseExited(with event: NSEvent) {
super.mouseExited(with: event)
if usesPointingHandCursor {
NSCursor.arrow.set()
}
onHoverChanged?(false)
}
}
Expand Down
2 changes: 1 addition & 1 deletion TaskMenu/Views/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Views render the AppKit menu-bar popover and settings UI. Keep business behavior

## Files

- `AppKitTaskUIHelpers.swift` - shared AppKit controls, SF Symbol helpers, layout pinning, hover handling, menu actions, and Observation glue.
- `AppKitTaskUIHelpers.swift` - shared AppKit controls, SF Symbol helpers, layout pinning, hover handling, opt-in pointing-hand cursor, menu actions, and Observation glue.
- `TaskPopoverViewController.swift` - signed-out, initial-loading, signed-in task list, bottom error strip, popover sizing, settings handoff, and popover surface styling.
- `TaskListAppKitViewController.swift` - task-list/detail coordination with an animated push/pop slide between the list page and the edit screen, list picker routing, search bar wiring, local list disclosure state, and `AppState` mutation wiring.
- `TaskListControlsAppKitViews.swift` - list picker, refresh spinner, overflow menu, search bar, and quick-add field AppKit views.
Expand Down
1 change: 1 addition & 0 deletions TaskMenu/Views/TaskDetailAppKitViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,7 @@ private final class TaskDetailSubtaskRow: NSView {
onPress: onToggle
)
toggle.contentTintColor = task.isCompleted ? .controlAccentColor : .secondaryLabelColor
toggle.usesPointingHandCursor = true
NSLayoutConstraint.activate([
toggle.widthAnchor.constraint(equalToConstant: 22),
toggle.heightAnchor.constraint(equalToConstant: 22)
Expand Down
1 change: 1 addition & 0 deletions TaskMenu/Views/TaskListContentAppKitView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,7 @@ private final class TaskOutlineTaskCellView: NSTableCellView {
}
checkButton.contentTintColor = entry.task.isCompleted ? .controlAccentColor : .secondaryLabelColor
checkButton.refusesFirstResponder = true
checkButton.usesPointingHandCursor = true
NSLayoutConstraint.activate([
checkButton.widthAnchor.constraint(equalToConstant: 26),
checkButton.heightAnchor.constraint(lessThanOrEqualToConstant: 24)
Expand Down
1 change: 1 addition & 0 deletions TaskMenuTests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Unit tests cover model behavior, app state, services, pure view helpers, and app
- `GitHubUpdateCheckerTests.swift` - semantic-version parsing, GitHub release decoding, update-check persistence, throttling, launch-alert suppression, and AppState update outcomes.
- `DateFormattingTests.swift` - RFC 3339, Google due-date, display, and relative-date behavior.
- `TaskListViewTests.swift`, `TaskDetailViewTests.swift`, and `MenuBarWindowChromeTests.swift` - shared task presentation helpers and AppKit glass/window behavior.
- `TaskMenuActionButtonTests.swift` - pointing-hand cursor opt-in on `TaskMenuActionButton` (cursor tracking areas and `cursorUpdate` behavior).
- `MetricKitPayloadStoreTests.swift` - local payload persistence.
- `TaskMenuAppTests.swift` - app/app-delegate construction seams, launch UI mode parsing, the automatic update-check loop, and update-alert choice mapping.
- `SettingsLaunchAtLoginTests.swift` - the pure launch-at-login status decision (`requiresApproval` notice) behind the Settings toggle.
Expand Down
60 changes: 60 additions & 0 deletions TaskMenuTests/TaskMenuActionButtonTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import AppKit
import XCTest
@testable import TaskMenu

@MainActor
final class TaskMenuActionButtonTests: XCTestCase {
private func makeMouseMovedEvent() throws -> NSEvent {
let event = NSEvent.mouseEvent(
with: .mouseMoved,
location: NSPoint(x: 5, y: 5),
modifierFlags: [],
timestamp: 0,
windowNumber: 0,
context: nil,
eventNumber: 0,
clickCount: 0,
pressure: 0
)
return try XCTUnwrap(event)
}

func testPointingHandCursorIsOptOutByDefault() {
let button = TaskMenuActionButton(symbolName: "circle")
button.frame = NSRect(x: 0, y: 0, width: 26, height: 24)
button.updateTrackingAreas()

XCTAssertFalse(button.usesPointingHandCursor)
XCTAssertTrue(button.trackingAreas.allSatisfy { !$0.options.contains(.cursorUpdate) })
}

func testEnablingPointingHandCursorAddsCursorUpdateTracking() {
let button = TaskMenuActionButton(symbolName: "circle")
button.frame = NSRect(x: 0, y: 0, width: 26, height: 24)

button.usesPointingHandCursor = true

XCTAssertTrue(button.trackingAreas.contains { $0.options.contains(.cursorUpdate) })
}

func testCursorUpdateSetsPointingHandWhenEnabled() throws {
let button = TaskMenuActionButton(symbolName: "circle")
button.usesPointingHandCursor = true
defer { NSCursor.arrow.set() }

NSCursor.arrow.set()
button.cursorUpdate(with: try makeMouseMovedEvent())

XCTAssertEqual(NSCursor.current, NSCursor.pointingHand)
}

func testCursorUpdateLeavesDefaultCursorWhenDisabled() throws {
let button = TaskMenuActionButton(symbolName: "circle")
defer { NSCursor.arrow.set() }

NSCursor.arrow.set()
button.cursorUpdate(with: try makeMouseMovedEvent())

XCTAssertNotEqual(NSCursor.current, NSCursor.pointingHand)
}
}
Loading