Skip to content
Merged
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
9 changes: 8 additions & 1 deletion MiddleDrag.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,10 @@
Models/GestureModels.swift,
Models/TouchModels.swift,
UI/AlertHelper.swift,
UI/HotKeyRecorderView.swift,
UI/MenuBarController.swift,
Utilities/AnalyticsManager.swift,
Utilities/GlobalHotKeyManager.swift,
Utilities/LaunchAtLoginManager.swift,
Utilities/PreferencesManager.swift,
Utilities/ScreenHelper.swift,
Expand All @@ -90,6 +92,8 @@
MiddleDragTests/DeviceMonitorTests.swift,
MiddleDragTests/GestureModelsTests.swift,
MiddleDragTests/GestureRecognizerTests.swift,
MiddleDragTests/GlobalHotKeyManagerTests.swift,
MiddleDragTests/HotKeyRecorderViewTests.swift,
MiddleDragTests/LaunchAtLoginManagerTests.swift,
MiddleDragTests/MenuBarControllerTests.swift,
MiddleDragTests/Mocks/MockDeviceMonitor.swift,
Expand All @@ -109,7 +113,6 @@
membershipExceptions = (
Debug.xcconfig,
Release.xcconfig,
Secrets.xcconfig,
);
target = 1A0000011 /* MiddleDrag */;
};
Expand All @@ -131,6 +134,8 @@
MiddleDragTests/DeviceMonitorTests.swift,
MiddleDragTests/GestureModelsTests.swift,
MiddleDragTests/GestureRecognizerTests.swift,
MiddleDragTests/GlobalHotKeyManagerTests.swift,
MiddleDragTests/HotKeyRecorderViewTests.swift,
MiddleDragTests/LaunchAtLoginManagerTests.swift,
MiddleDragTests/MenuBarControllerTests.swift,
MiddleDragTests/Mocks/MockDeviceMonitor.swift,
Expand All @@ -145,8 +150,10 @@
Models/GestureModels.swift,
Models/TouchModels.swift,
UI/AlertHelper.swift,
UI/HotKeyRecorderView.swift,
UI/MenuBarController.swift,
Utilities/AnalyticsManager.swift,
Utilities/GlobalHotKeyManager.swift,
Utilities/LaunchAtLoginManager.swift,
Utilities/PreferencesManager.swift,
Utilities/ScreenHelper.swift,
Expand Down
39 changes: 39 additions & 0 deletions MiddleDrag/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Cocoa
import Carbon.HIToolbox
import MiddleDragCore

/// Main application delegate
Expand All @@ -18,6 +19,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private var preferences: UserPreferences!

private var accessibilityMonitor: AccessibilityMonitor?

private var toggleHotKeyID: UInt32 = 0
private var menuBarHotKeyID: UInt32 = 0

// MARK: - Application Lifecycle

Expand Down Expand Up @@ -111,6 +115,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
)
Log.info("Menu bar controller initialized", category: .app)

// Register global hotkeys
registerHotKeys()

// Set up notification observers
setupNotifications()

Expand Down Expand Up @@ -160,6 +167,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
return true
}

func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool {
menuBarController?.showMenuBarIcon()
return false
}

// MARK: - Setup

private func setupNotifications() {
Expand All @@ -177,6 +189,32 @@ class AppDelegate: NSObject, NSApplicationDelegate {
object: nil
)
}

/// Register (or re-register) global hotkeys from current preferences
private func registerHotKeys() {
// Unregister existing
if toggleHotKeyID != 0 {
GlobalHotKeyManager.shared.unregister(id: toggleHotKeyID)
}
if menuBarHotKeyID != 0 {
GlobalHotKeyManager.shared.unregister(id: menuBarHotKeyID)
}

// Register from preferences
toggleHotKeyID = GlobalHotKeyManager.shared.register(
keyCode: preferences.toggleHotKey.keyCode,
modifiers: preferences.toggleHotKey.carbonModifiers
) { [weak self] in
self?.menuBarController?.toggleEnabled()
}

menuBarHotKeyID = GlobalHotKeyManager.shared.register(
keyCode: preferences.menuBarHotKey.keyCode,
modifiers: preferences.menuBarHotKey.carbonModifiers
) { [weak self] in
self?.menuBarController?.toggleMenuBarVisibility()
}
}

// MARK: - Notification Handlers

Expand All @@ -185,6 +223,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
preferences = newPreferences
PreferencesManager.shared.savePreferences(preferences)
multitouchManager.updateConfiguration(preferences.gestureConfig)
registerHotKeys()
Log.info("Preferences updated", category: .app)
}
}
Expand Down
4 changes: 2 additions & 2 deletions MiddleDrag/Managers/MultitouchManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -870,9 +870,9 @@ extension MultitouchManager: DeviceMonitorDelegate {

gestureQueue.async { [weak self] in
if let data = touchData {
data.withUnsafeBytes { rawBuffer in
unsafe data.withUnsafeBytes { rawBuffer in
guard let baseAddress = rawBuffer.baseAddress else { return }
let buffer = UnsafeMutableRawPointer(mutating: baseAddress)
let buffer = unsafe UnsafeMutableRawPointer(mutating: baseAddress)
unsafe self?.gestureRecognizer.processTouches(
buffer, count: touchCount, timestamp: timestamp, modifierFlags: modifierFlags)
}
Expand Down
6 changes: 3 additions & 3 deletions MiddleDrag/MiddleDragTests/AlertHelperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ final class AlertHelperTests: XCTestCase {
AlertHelper.presenter = mockPresenter

// Save and replace SystemGestureHelper settings provider
originalSettingsProvider = SystemGestureHelper.settingsProvider
originalSettingsProvider = unsafe SystemGestureHelper.settingsProvider
mockSettingsProvider = MockTrackpadSettingsProvider()
SystemGestureHelper.settingsProvider = mockSettingsProvider
unsafe SystemGestureHelper.settingsProvider = mockSettingsProvider
}

override func tearDown() {
// Restore originals
AlertHelper.presenter = originalPresenter
SystemGestureHelper.settingsProvider = originalSettingsProvider
unsafe SystemGestureHelper.settingsProvider = originalSettingsProvider
mockPresenter = nil
mockSettingsProvider = nil
super.tearDown()
Expand Down
16 changes: 8 additions & 8 deletions MiddleDrag/MiddleDragTests/DeviceMonitorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ import XCTest
}

func testStartStopStartDoesNotCrash() throws {
try requireUnsafeMultitouchTestsEnabled()
try unsafe requireUnsafeMultitouchTestsEnabled()
// Should be able to restart the monitor
unsafe XCTAssertNoThrow(monitor.start())
unsafe XCTAssertNoThrow(monitor.stop())
Expand Down Expand Up @@ -122,7 +122,7 @@ import XCTest
// MARK: - Multiple Instance Tests

func testMultipleInstancesDoNotCrash() throws {
try requireUnsafeMultitouchTestsEnabled()
try unsafe requireUnsafeMultitouchTestsEnabled()
// Create multiple monitors - only first should own global reference
let monitor2 = unsafe DeviceMonitor()
let monitor3 = unsafe DeviceMonitor()
Expand Down Expand Up @@ -208,7 +208,7 @@ import XCTest
}

func testRapidStartStopCyclesDoNotCrash() throws {
try requireUnsafeMultitouchTestsEnabled()
try unsafe requireUnsafeMultitouchTestsEnabled()
// Simulates the race condition scenario where rapid restart cycles
// could cause the framework's internal thread to access deallocated resources.
// The fix adds delays to prevent this, so rapid cycles should be safe.
Expand All @@ -222,7 +222,7 @@ import XCTest
}

func testStopSeparatesCallbackUnregistrationFromDeviceStop() throws {
try requireUnsafeMultitouchTestsEnabled()
try unsafe requireUnsafeMultitouchTestsEnabled()
// This test exercises the code path where:
// 1. Callbacks are unregistered first (MTUnregisterContactFrameCallback)
// 2. A delay occurs (Thread.sleep)
Expand All @@ -241,7 +241,7 @@ import XCTest
}

func testConcurrentStopDoesNotCrash() throws {
try requireUnsafeMultitouchTestsEnabled()
try unsafe requireUnsafeMultitouchTestsEnabled()
// Test that even if something tries to access the monitor during stop,
// it doesn't crash. This simulates what happens when the framework's
// internal thread is still processing while we stop.
Expand Down Expand Up @@ -287,7 +287,7 @@ import XCTest
}

func testRapidRestartCyclesWithDelayDoNotCrash() throws {
try requireUnsafeMultitouchTestsEnabled()
try unsafe requireUnsafeMultitouchTestsEnabled()
// Simulates the exact scenario from the bug report:
// Rapid restart cycles during connectivity changes causing
// gDeviceMonitor to become nil while callbacks are still in-flight.
Expand Down Expand Up @@ -318,7 +318,7 @@ import XCTest
}

func testConcurrentStartStopDoesNotCrash() throws {
try requireUnsafeMultitouchTestsEnabled()
try unsafe requireUnsafeMultitouchTestsEnabled()
// Test that concurrent start/stop operations on the same instance don't crash.
// NOTE: Concurrent start/stop on the same instance may leave it in an inconsistent
// state, but it should NOT crash due to the locking mechanism protecting global state.
Expand Down Expand Up @@ -353,7 +353,7 @@ import XCTest
}

func testMultipleMonitorCreationDuringCleanup() throws {
try requireUnsafeMultitouchTestsEnabled()
try unsafe requireUnsafeMultitouchTestsEnabled()
// Test that creating new monitors while the old one is being cleaned up
// doesn't cause a crash. This tests the gPendingCleanup mechanism.
unsafe monitor.start()
Expand Down
2 changes: 1 addition & 1 deletion MiddleDrag/MiddleDragTests/GestureRecognizerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ final class GestureRecognizerTests: XCTestCase {
let count = touches.count
guard count > 0 else {
// Non-null placeholder; processTouches won't dereference when count == 0.
return (UnsafeMutableRawPointer(bitPattern: 1)!, 0, {})
return unsafe (UnsafeMutableRawPointer(bitPattern: 1)!, 0, {})
}

let pointer = UnsafeMutablePointer<MTTouch>.allocate(capacity: count)
Expand Down
Loading
Loading