Skip to content

Commit 793320d

Browse files
Merge branch 'main' into remove-analytics-manager-shim-2519353167747654180
2 parents d6bba20 + 990403f commit 793320d

8 files changed

Lines changed: 284 additions & 223 deletions

File tree

.github/workflows/objective-c-xcode.yml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,25 @@ permissions:
44

55
on:
66
push:
7-
branches: [main]
7+
branches:
8+
- '**'
89
pull_request:
9-
branches: [main]
10+
branches:
11+
- '**'
1012

1113
jobs:
14+
15+
pre_job:
16+
runs-on: ubuntu-latest
17+
outputs:
18+
should_skip: ${{ steps.skip_check.outputs.should_skip }}
19+
steps:
20+
- id: skip_check
21+
uses: fkirc/skip-duplicate-actions@master
22+
1223
build:
24+
needs: pre_job
25+
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
1326
name: Build MiddleDrag
1427
runs-on: macos-15
1528

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ xcuserdata/
2020
*.ipa
2121
*.dSYM.zip
2222
*.dSYM
23+
.vscode/settings.json
Lines changed: 92 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,95 @@
1-
import Foundation
21
import CoreGraphics
2+
import Foundation
33

44
/// Manages gesture recognition from touch input
55
class GestureRecognizer {
6-
6+
77
// MARK: - Properties
8-
8+
99
/// Configuration for gesture detection
1010
var configuration = GestureConfiguration()
11-
11+
1212
/// Current gesture state
1313
private(set) var state: GestureState = .idle
14-
14+
1515
/// Delegate for gesture events
1616
weak var delegate: GestureRecognizerDelegate?
17-
17+
1818
// Position tracking
1919
private var lastFingerPositions: [MTPoint] = []
2020
private var gestureStartTime: Double = 0
2121
private var gestureStartPosition: MTPoint?
2222
private var lastCentroid: MTPoint?
2323
private var frameCount: Int = 0
24-
24+
2525
// Stability tracking - prevents false gesture ends during brief state transitions
2626
private var stableFrameCount: Int = 0
27-
27+
28+
// Cooldown after 4-finger cancellation
29+
// Prevents accidental gesture triggers when lifting one finger during Mission Control
30+
private var isInCancellationCooldown: Bool = false
31+
2832
// MARK: - Public Interface
29-
33+
3034
/// Process new touch data from the multitouch device
3135
/// - Parameters:
3236
/// - touches: Raw pointer to touch data array
3337
/// - count: Number of touches in the array
3438
/// - timestamp: Timestamp of the touch frame
3539
func processTouches(_ touches: UnsafeMutableRawPointer, count: Int, timestamp: Double) {
3640
let touchArray = touches.bindMemory(to: MTTouch.self, capacity: count)
37-
41+
3842
// Collect only valid touching fingers (state 3 = touching down, state 4 = active)
3943
// Skip state 5 (lifting), 6 (lingering), 7 (gone)
4044
var validFingers: [MTPoint] = []
41-
45+
4246
for i in 0..<count {
4347
let touch = touchArray[i]
4448
if touch.state == 3 || touch.state == 4 {
4549
validFingers.append(touch.normalizedVector.position)
4650
}
4751
}
48-
52+
4953
let fingerCount = validFingers.count
50-
51-
if fingerCount >= 3 {
54+
55+
// ALWAYS cancel on 4+ fingers regardless of configuration
56+
// This ensures Mission Control and other system gestures always work
57+
if fingerCount >= 4 {
58+
if state != .idle {
59+
handleGestureCancel()
60+
}
61+
// Enter cooldown to prevent restart when finger is briefly lifted
62+
isInCancellationCooldown = true
63+
return
64+
}
65+
66+
// Clear cooldown when finger count drops to 0-2,
67+
// or when finger count is 3 and we're idle (so user can start a new gesture)
68+
if fingerCount <= 2 || (fingerCount == 3 && state == .idle) {
69+
isInCancellationCooldown = false
70+
}
71+
72+
// Process exactly 3-finger gestures only
73+
// 4+ fingers are cancelled above, 0-2 fingers end the gesture below
74+
// Note: requiresExactlyThreeFingers no longer has an effect since we always
75+
// require exactly 3 to preserve Mission Control - could be deprecated
76+
let isValidThreeFingerGesture = !isInCancellationCooldown && fingerCount == 3
77+
78+
if isValidThreeFingerGesture {
5279
handleThreeFingerGesture(fingers: validFingers, timestamp: timestamp)
5380
} else if state != .idle {
54-
// Only end gesture if we've been below 3 fingers for multiple frames
55-
// This prevents ending during brief state transitions
81+
// Gesture state changed - finger count dropped below 3
82+
// (4+ fingers case is handled by cancellation above before we get here)
83+
// Use stable frame count to prevent false ends during brief transitions
5684
stableFrameCount += 1
5785
if stableFrameCount >= 2 {
5886
handleGestureEnd(timestamp: timestamp)
5987
}
6088
}
61-
89+
6290
frameCount += 1
6391
}
64-
92+
6593
/// Reset gesture recognition state
6694
func reset() {
6795
state = .idle
@@ -71,15 +99,16 @@ class GestureRecognizer {
7199
gestureStartTime = 0
72100
frameCount = 0
73101
stableFrameCount = 0
102+
isInCancellationCooldown = false // Clear cooldown on reset
74103
}
75-
104+
76105
// MARK: - Private Methods
77-
106+
78107
private func handleThreeFingerGesture(fingers: [MTPoint], timestamp: Double) {
79108
stableFrameCount = 0 // Reset since we have 3 fingers
80-
109+
81110
let centroid = calculateCentroid(fingers: fingers)
82-
111+
83112
// Check for large centroid jumps (finger added/removed causing position shift)
84113
if let last = lastCentroid {
85114
let jump = centroid.distance(to: last)
@@ -90,7 +119,7 @@ class GestureRecognizer {
90119
return
91120
}
92121
}
93-
122+
94123
switch state {
95124
case .idle:
96125
// Start new gesture
@@ -100,27 +129,27 @@ class GestureRecognizer {
100129
lastCentroid = centroid
101130
lastFingerPositions = fingers
102131
delegate?.gestureRecognizerDidStart(self, at: centroid)
103-
132+
104133
case .possibleTap:
105134
// Check if we should transition to drag
106135
guard let startPos = gestureStartPosition else { return }
107136
let movement = startPos.distance(to: centroid)
108137
let elapsed = timestamp - gestureStartTime
109-
138+
110139
if movement > configuration.moveThreshold || elapsed > configuration.tapThreshold {
111140
state = .dragging
112141
lastCentroid = centroid
113142
delegate?.gestureRecognizerDidBeginDragging(self)
114143
} else {
115144
lastCentroid = centroid
116145
}
117-
146+
118147
case .dragging:
119148
// Calculate delta from last frame
120149
if let last = lastCentroid {
121150
let deltaX = centroid.x - last.x
122151
let deltaY = centroid.y - last.y
123-
152+
124153
// Only process small deltas (real movement, not jumps)
125154
if abs(deltaX) < 0.03 && abs(deltaY) < 0.03 {
126155
if abs(deltaX) > 0.0001 || abs(deltaY) > 0.0001 {
@@ -137,17 +166,17 @@ class GestureRecognizer {
137166
}
138167
}
139168
lastCentroid = centroid
140-
169+
141170
case .waitingForRelease:
142171
break
143172
}
144-
173+
145174
lastFingerPositions = fingers
146175
}
147-
176+
148177
private func handleGestureEnd(timestamp: Double) {
149178
let elapsed = timestamp - gestureStartTime
150-
179+
151180
switch state {
152181
case .possibleTap:
153182
if elapsed < configuration.tapThreshold {
@@ -158,10 +187,26 @@ class GestureRecognizer {
158187
default:
159188
break
160189
}
161-
190+
191+
reset()
192+
}
193+
194+
/// Cancel gesture without completing it (e.g., when 4th finger detected)
195+
private func handleGestureCancel() {
196+
switch state {
197+
case .possibleTap:
198+
// Cancel the possible tap - notify delegate so it can reset state
199+
delegate?.gestureRecognizerDidCancel(self)
200+
case .dragging:
201+
// Cancel the drag - don't complete it normally
202+
delegate?.gestureRecognizerDidCancelDragging(self)
203+
default:
204+
break
205+
}
206+
162207
reset()
163208
}
164-
209+
165210
private func calculateCentroid(fingers: [MTPoint]) -> MTPoint {
166211
let sumX = fingers.reduce(0) { $0 + $1.x }
167212
let sumY = fingers.reduce(0) { $0 + $1.y }
@@ -179,17 +224,17 @@ struct GestureData {
179224
let fingerCount: Int
180225
let startPosition: MTPoint?
181226
let lastPosition: MTPoint
182-
227+
183228
/// Calculate frame-to-frame delta with sensitivity applied
184229
func frameDelta(from configuration: GestureConfiguration) -> (x: CGFloat, y: CGFloat) {
185230
let deltaX = CGFloat(centroid.x - lastPosition.x)
186231
let deltaY = CGFloat(centroid.y - lastPosition.y)
187-
232+
188233
// Reject large deltas (likely jumps from finger changes)
189234
if abs(deltaX) > 0.03 || abs(deltaY) > 0.03 {
190235
return (0, 0)
191236
}
192-
237+
193238
let sensitivity = CGFloat(configuration.effectiveSensitivity(for: velocity))
194239
return (deltaX * sensitivity, deltaY * sensitivity)
195240
}
@@ -201,16 +246,22 @@ struct GestureData {
201246
protocol GestureRecognizerDelegate: AnyObject {
202247
/// Called when a gesture starts (3 fingers detected)
203248
func gestureRecognizerDidStart(_ recognizer: GestureRecognizer, at position: MTPoint)
204-
249+
205250
/// Called when a tap gesture is recognized
206251
func gestureRecognizerDidTap(_ recognizer: GestureRecognizer)
207-
252+
208253
/// Called when dragging begins
209254
func gestureRecognizerDidBeginDragging(_ recognizer: GestureRecognizer)
210-
255+
211256
/// Called during drag with movement data
212257
func gestureRecognizerDidUpdateDragging(_ recognizer: GestureRecognizer, with data: GestureData)
213-
214-
/// Called when dragging ends
258+
259+
/// Called when dragging ends normally (user lifted fingers)
215260
func gestureRecognizerDidEndDragging(_ recognizer: GestureRecognizer)
261+
262+
/// Called when gesture is cancelled from early state (e.g., possibleTap when 4th finger added)
263+
func gestureRecognizerDidCancel(_ recognizer: GestureRecognizer)
264+
265+
/// Called when dragging is cancelled (e.g., 4th finger added for Mission Control)
266+
func gestureRecognizerDidCancelDragging(_ recognizer: GestureRecognizer)
216267
}

0 commit comments

Comments
 (0)