@@ -479,26 +479,33 @@ extension MenuBarItemManager {
479479// MARK: - Async Waiters
480480
481481extension MenuBarItemManager {
482+ /// Waits asynchronously for the given operation to complete.
483+ ///
484+ /// - Parameters:
485+ /// - timeout: Amount of time to wait before throwing an error.
486+ /// - operation: The operation to perform.
487+ private func waitWithTask( timeout: Duration ? , operation: @escaping @Sendable ( ) async throws -> Void ) async throws {
488+ let task = if let timeout {
489+ Task ( timeout: timeout, operation: operation)
490+ } else {
491+ Task ( operation: operation)
492+ }
493+ try await task. value
494+ }
495+
482496 /// Waits asynchronously for all menu bar items to stop moving.
483497 ///
484498 /// - Parameter timeout: Amount of time to wait before throwing an error.
485499 func waitForItemsToStopMoving( timeout: Duration ? = nil ) async throws {
486- let taskBody : @Sendable ( ) async throws -> Void = {
487- while await self . isMovingItem {
500+ try await waitWithTask ( timeout: timeout) { [ weak self] in
501+ guard let self else {
502+ return
503+ }
504+ while await isMovingItem {
488505 try Task . checkCancellation ( )
489506 try await Task . sleep ( for: . milliseconds( 10 ) )
490507 }
491508 }
492- let checkTask = if let timeout {
493- Task ( timeout: timeout) {
494- try await taskBody ( )
495- }
496- } else {
497- Task {
498- try await taskBody ( )
499- }
500- }
501- try await checkTask. value
502509 }
503510
504511 /// Waits asynchronously for the mouse to stop moving.
@@ -507,10 +514,13 @@ extension MenuBarItemManager {
507514 /// - threshold: A threshold to use to determine whether the mouse has stopped moving.
508515 /// - timeout: Amount of time to wait before throwing an error.
509516 func waitForMouseToStopMoving( threshold: TimeInterval = 0.1 , timeout: Duration ? = nil ) async throws {
510- let taskBody : @Sendable ( ) async throws -> Void = {
517+ try await waitWithTask ( timeout: timeout) { [ weak self] in
518+ guard let self else {
519+ return
520+ }
511521 while true {
512522 try Task . checkCancellation ( )
513- guard let date = await self . lastMouseMoveStartDate else {
523+ guard let date = await lastMouseMoveStartDate else {
514524 break
515525 }
516526 if Date . now. timeIntervalSince ( date) > threshold {
@@ -519,16 +529,34 @@ extension MenuBarItemManager {
519529 try await Task . sleep ( for: . milliseconds( 10 ) )
520530 }
521531 }
522- let checkTask = if let timeout {
523- Task ( timeout: timeout) {
524- try await taskBody ( )
532+ }
533+
534+ /// Waits asynchronously until no modifier keys are pressed.
535+ ///
536+ /// - Parameter timeout: Amount of time to wait before throwing an error.
537+ func waitForNoModifiersPressed( timeout: Duration ? = nil ) async throws {
538+ try await waitWithTask ( timeout: timeout) {
539+ // Return early if no flags are pressed.
540+ if NSEvent . modifierFlags. isEmpty {
541+ return
525542 }
526- } else {
527- Task {
528- try await taskBody ( )
543+
544+ var cancellable : AnyCancellable ?
545+
546+ await withCheckedContinuation { continuation in
547+ cancellable = Publishers . Merge (
548+ UniversalEventMonitor . publisher ( for: . flagsChanged) ,
549+ RunLoopLocalEventMonitor . publisher ( for: . flagsChanged, mode: . eventTracking)
550+ )
551+ . removeDuplicates ( )
552+ . sink { _ in
553+ if NSEvent . modifierFlags. isEmpty {
554+ cancellable? . cancel ( )
555+ continuation. resume ( )
556+ }
557+ }
529558 }
530559 }
531- try await checkTask. value
532560 }
533561}
534562
@@ -1051,6 +1079,9 @@ extension MenuBarItemManager {
10511079 }
10521080
10531081 do {
1082+ // Order of these waiters matters, as the modifiers could be released
1083+ // while the mouse is still moving.
1084+ try await waitForNoModifiersPressed ( )
10541085 try await waitForMouseToStopMoving ( )
10551086 } catch {
10561087 throw EventError ( code: . couldNotComplete, item: item)
0 commit comments