beatmatchr/Desktop/YabaiPro/Sources/TrackpadGestureManager.swift
BusyBee3333 7694d965c9 feat: Add structured signal editor with app dropdown and action builder
- Add AppDiscovery provider for running app enumeration
- Implement AppDropdownView with auto-launch functionality
- Create SignalAction models for 40+ yabai commands
- Build ActionBuilderView with nested parameter controls
- Add LiveShellPreview for real-time shell command generation
- Implement ActionValidator for conflict detection
- Add migration parser for existing raw action strings
- Include feature flag for safe rollout
- Maintain full backward compatibility
2025-12-31 01:44:13 -05:00

297 lines
11 KiB
Swift

//
// TrackpadGestureManager.swift
// YabaiPro
//
// Created by Jake Shore
// Copyright © 2024 Jake Shore. All rights reserved.
//
import Foundation
import AppKit
import ApplicationServices
import CoreGraphics
// CGEvent tap callback function
func eventTapCallback(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, userInfo: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
guard let userInfo = userInfo else { return Unmanaged.passUnretained(event) }
let manager = Unmanaged<TrackpadGestureManager>.fromOpaque(userInfo).takeUnretainedValue()
manager.handleCGEvent(event, type: type)
return Unmanaged.passUnretained(event)
}
class TrackpadGestureManager {
static let shared = TrackpadGestureManager()
private var eventTap: CFMachPort?
private var runLoopSource: CFRunLoopSource?
private var keyboardMonitor: Any?
// Resize state
private var isInResizeMode = false
private var initialResizePosition: CGPoint?
private var originalWindowFrame: CGRect?
private let yabaiRunner = YabaiCommandRunner.shared
private init() {}
func startMonitoring() {
print("🎯 Starting trackpad gesture monitoring...")
// Check if we have accessibility permissions
let hasAccessibility = AXIsProcessTrusted()
if !hasAccessibility {
print("⚠️ Accessibility permissions not granted - gesture monitoring disabled")
return
}
// Create CGEvent tap to capture scroll wheel events (which include multi-touch)
let eventMask: CGEventMask = 1 << CGEventType.scrollWheel.rawValue
eventTap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: eventMask,
callback: eventTapCallback,
userInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
if let tap = eventTap {
runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
CGEvent.tapEnable(tap: tap, enable: true)
print("✅ Trackpad gesture monitoring started with CGEvent tap")
} else {
print("❌ Failed to create CGEvent tap - may need Accessibility permissions")
}
// Keep keyboard test for debugging
keyboardMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.keyDown]) { [weak self] event in
if event.keyCode == 49 && event.modifierFlags.contains(.command) { // Space + Cmd
print("🎯 Test resize triggered via keyboard shortcut")
self?.triggerTestResize()
}
}
print("💡 Test resize with Cmd+Space")
}
func stopMonitoring() {
if let source = runLoopSource {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, .commonModes)
runLoopSource = nil
}
if let tap = eventTap {
CGEvent.tapEnable(tap: tap, enable: false)
eventTap = nil
}
if let monitor = keyboardMonitor {
NSEvent.removeMonitor(monitor)
keyboardMonitor = nil
}
print("🛑 Trackpad gesture monitoring stopped")
}
func handleCGEvent(_ event: CGEvent, type: CGEventType) {
// Only process if gestures are enabled in settings
guard ComprehensiveSettingsViewModel.shared.twoFingerResizeEnabled else {
return
}
if type == .scrollWheel {
handleScrollWheelEvent(event)
}
}
private func handleScrollWheelEvent(_ event: CGEvent) {
let pointDeltaX = event.getDoubleValueField(.scrollWheelEventPointDeltaAxis2)
let pointDeltaY = event.getDoubleValueField(.scrollWheelEventPointDeltaAxis1)
let isMomentum = event.getIntegerValueField(.scrollWheelEventMomentumPhase) != 0
if abs(pointDeltaX) > 0.1 || abs(pointDeltaY) > 0.1 {
print("🎯 Scroll gesture detected - deltaX: \(pointDeltaX), deltaY: \(pointDeltaY)")
let mouseLocation = NSEvent.mouseLocation
let centerPoint = CGPoint(x: mouseLocation.x, y: mouseLocation.y)
if !isInResizeMode {
startResizeIfNeeded(at: centerPoint)
} else {
let sensitivity = ComprehensiveSettingsViewModel.shared.resizeSensitivity
let adjustedDeltaX = pointDeltaX * sensitivity * 2
let adjustedDeltaY = pointDeltaY * sensitivity * 2
let newPoint = CGPoint(
x: centerPoint.x + adjustedDeltaX,
y: centerPoint.y + adjustedDeltaY
)
continueResize(to: newPoint)
}
}
if abs(pointDeltaX) < 0.01 && abs(pointDeltaY) < 0.01 && isInResizeMode && isMomentum {
endResize()
}
}
private func startResizeIfNeeded(at point: CGPoint) {
print("🎯 Starting resize check at point: \(point)")
Task {
guard let windowInfo = await getWindowUnderPoint(point) else {
print("🎯 No window found under cursor")
return
}
guard isWindowResizable(windowInfo) else { return }
print("🎯 Starting two-finger resize on window \(windowInfo.id)")
isInResizeMode = true
initialResizePosition = point
originalWindowFrame = CGRect(x: windowInfo.frame.x, y: windowInfo.frame.y,
width: windowInfo.frame.w, height: windowInfo.frame.h)
if ComprehensiveSettingsViewModel.shared.gestureHapticFeedbackEnabled {
NSHapticFeedbackManager.defaultPerformer.perform(.generic, performanceTime: .default)
}
}
}
private func continueResize(to point: CGPoint) {
guard isInResizeMode,
let initialPos = initialResizePosition,
let originalFrame = originalWindowFrame else { return }
Task {
guard let currentWindow = await getFocusedWindow() else {
print("🎯 No focused window found for resize")
return
}
let windowId = currentWindow.id
let deltaX = point.x - initialPos.x
let deltaY = point.y - initialPos.y
let direction = ResizeDirection.from(deltaX: deltaX, deltaY: deltaY)
let sensitivity = ComprehensiveSettingsViewModel.shared.resizeSensitivity
do {
switch direction {
case .right:
let newWidth = originalFrame.width + (deltaX * sensitivity)
try await yabaiRunner.resizeWindow(id: windowId, width: newWidth, height: originalFrame.height)
case .left:
let newWidth = originalFrame.width + (-deltaX * sensitivity)
let newX = originalFrame.minX + (deltaX * sensitivity)
try await yabaiRunner.resizeWindow(id: windowId, width: newWidth, height: originalFrame.height)
try await yabaiRunner.moveWindow(id: windowId, x: newX, y: nil)
case .up:
let newHeight = originalFrame.height + (-deltaY * sensitivity)
let newY = originalFrame.minY + (deltaY * sensitivity)
try await yabaiRunner.resizeWindow(id: windowId, width: originalFrame.width, height: newHeight)
try await yabaiRunner.moveWindow(id: windowId, x: nil, y: newY)
case .down:
let newHeight = originalFrame.height + (deltaY * sensitivity)
try await yabaiRunner.resizeWindow(id: windowId, width: originalFrame.width, height: newHeight)
case .diagonal:
let scaleFactor = max(abs(deltaX), abs(deltaY)) * sensitivity / 100.0
let newWidth = originalFrame.width * (1 + scaleFactor)
let newHeight = originalFrame.height * (1 + scaleFactor)
try await yabaiRunner.resizeWindow(id: windowId, width: newWidth, height: newHeight)
}
} catch {
print("Error resizing window: \(error)")
}
}
}
func endResize() {
if isInResizeMode {
print("🎯 Ending two-finger resize")
isInResizeMode = false
initialResizePosition = nil
originalWindowFrame = nil
if ComprehensiveSettingsViewModel.shared.gestureHapticFeedbackEnabled {
NSHapticFeedbackManager.defaultPerformer.perform(.generic, performanceTime: .default)
}
}
}
private func getFocusedWindow() async -> YabaiWindowJSON? {
do {
let windowsData = try await yabaiRunner.queryWindows()
let windows = try JSONDecoder().decode([YabaiWindowJSON].self, from: windowsData)
return windows.first { $0.hasFocus }
} catch {
print("Error getting focused window: \(error)")
return nil
}
}
private func triggerTestResize() {
let mouseLocation = NSEvent.mouseLocation
print("🎯 Testing resize at mouse location: \(mouseLocation)")
Task {
guard let windowInfo = await getWindowUnderPoint(mouseLocation) else {
print("🎯 No window found at mouse location")
return
}
print("🎯 Found window: \(windowInfo.id) - testing resize")
do {
let newWidth = windowInfo.frame.w + 50
let newHeight = windowInfo.frame.h + 30
try await yabaiRunner.resizeWindow(id: windowInfo.id, width: newWidth, height: newHeight)
print("✅ Test resize successful - window should be larger")
} catch {
print("❌ Test resize failed: \(error)")
}
}
}
private func getWindowUnderPoint(_ point: CGPoint) async -> YabaiWindowJSON? {
do {
let windowsData = try await yabaiRunner.queryWindows()
let windows = try JSONDecoder().decode([YabaiWindowJSON].self, from: windowsData)
for window in windows {
let frame = CGRect(x: window.frame.x, y: window.frame.y,
width: window.frame.w, height: window.frame.h)
if frame.contains(point) {
return window
}
}
return windows.first { $0.hasFocus }
} catch {
print("Error getting windows: \(error)")
return nil
}
}
private func isWindowResizable(_ window: YabaiWindowJSON) -> Bool {
return window.id > 100
}
}
enum ResizeDirection {
case right, left, up, down, diagonal
static func from(deltaX: CGFloat, deltaY: CGFloat) -> ResizeDirection {
let absX = abs(deltaX)
let absY = abs(deltaY)
if absX > absY * 2 { return deltaX > 0 ? .right : .left }
if absY > absX * 2 { return deltaY > 0 ? .up : .down }
return .diagonal
}
}