@@ -18,200 +18,17 @@ struct MiddleQuitApp: App {
1818 preferences: appDelegate. preferences,
1919 onToggleShowIcon: { show in
2020 appDelegate. applyStatusItemVisibility ( show: show)
21+ } ,
22+ isLaunchAtLoginEnabled: {
23+ appDelegate. launchAtLogin. isEnabled
24+ } ,
25+ onToggleLaunchAtLogin: {
26+ _ = appDelegate. launchAtLogin. toggle ( )
2127 }
2228 )
2329 }
2430 . windowResizability ( . contentSize)
2531 }
2632}
2733
28- final class AppDelegate : NSObject , NSApplicationDelegate {
29- let preferences = Preferences ( )
30- private let eventTapManager = EventTapManager ( )
31- private let dockHelper = DockAccessibilityHelper ( )
32- private let quitController = QuitController ( )
33- private let launchAtLogin = LaunchAtLoginManager ( )
34- private var statusController : StatusItemController !
35-
36- private var axPollingTimer : Timer ?
37- private var hasStartedEventTap = false
38-
39- func applicationDidFinishLaunching( _ notification: Notification ) {
40- statusController = StatusItemController (
41- preferences: preferences,
42- onToggleShowIcon: { [ weak self] show in
43- self ? . applyStatusItemVisibility ( show: show)
44- } ,
45- onOpenAccessibility: { [ weak self] in
46- self ? . promptForAccessibilityAndAutoStart ( )
47- } ,
48- onToggleLaunchAtLogin: { [ weak self] in
49- guard let self else { return }
50- let result = self . launchAtLogin. toggle ( )
51- switch result {
52- case . requiresApproval:
53- self . launchAtLogin. openLoginItemsSettingsIfAvailable ( )
54- case . failed( let error) :
55- let alert = NSAlert ( )
56- alert. messageText = " Couldn’t Update Login Item "
57- alert. informativeText = error. localizedDescription
58- alert. alertStyle = . warning
59- alert. addButton ( withTitle: " OK " )
60- alert. runModal ( )
61- case . unavailable:
62- let alert = NSAlert ( )
63- alert. messageText = " Not Supported on This macOS Version "
64- alert. informativeText = " Enabling launch at login without a helper app requires macOS 13 or later. "
65- alert. alertStyle = . informational
66- alert. addButton ( withTitle: " OK " )
67- alert. runModal ( )
68- default :
69- break
70- }
71- } ,
72- isLaunchAtLoginEnabled: { [ weak self] in
73- return self ? . launchAtLogin. isEnabled ?? false
74- } ,
75- onQuit: {
76- NSApp . terminate ( nil )
77- }
78- )
79-
80- applyStatusItemVisibility ( show: preferences. showStatusItem)
81- ensureAccessibilityAndStart ( )
82- }
83-
84- // MARK: - Accessibility flow (Option C)
85-
86- private func promptForAccessibilityAndAutoStart( ) {
87- NSApp . activate ( ignoringOtherApps: true )
88-
89- let alert = NSAlert ( )
90- alert. messageText = " Accessibility Permission "
91- alert. informativeText = " MiddleQuit needs Accessibility permission to handle mouse clicks. macOS will show a prompt. After allowing, MiddleQuit will become active automatically. "
92- alert. alertStyle = . informational
93- alert. addButton ( withTitle: " Continue " )
94- alert. addButton ( withTitle: " Cancel " )
95- let response = alert. runModal ( )
96- guard response == . alertFirstButtonReturn else { return }
97-
98- DockAccessibilityHelper . requestAXPermissionIfNeeded ( )
99- beginAXTrustPolling ( timeout: 30.0 , interval: 0.5 )
100- }
101-
102- private func beginAXTrustPolling( timeout: TimeInterval , interval: TimeInterval ) {
103- axPollingTimer? . invalidate ( )
104-
105- if DockAccessibilityHelper . isAXEnabled ( ) {
106- ensureAccessibilityAndStart ( )
107- return
108- }
109-
110- let deadline = Date ( ) . addingTimeInterval ( timeout)
111-
112- axPollingTimer = Timer . scheduledTimer ( withTimeInterval: interval, repeats: true ) { [ weak self] timer in
113- guard let self else { return }
114- if DockAccessibilityHelper . isAXEnabled ( ) {
115- timer. invalidate ( )
116- self . axPollingTimer = nil
117- self . ensureAccessibilityAndStart ( )
118- } else if Date ( ) >= deadline {
119- timer. invalidate ( )
120- self . axPollingTimer = nil
121- self . offerAccessibilitySettingsFallback ( )
122- }
123- }
124-
125- RunLoop . main. add ( axPollingTimer!, forMode: . common)
126- }
127-
128- private func offerAccessibilitySettingsFallback( ) {
129- let fallback = NSAlert ( )
130- fallback. messageText = " Accessibility Not Enabled "
131- fallback. informativeText = " You can enable Accessibility for MiddleQuit in System Settings. Would you like to open it now? "
132- fallback. alertStyle = . warning
133- fallback. addButton ( withTitle: " Open Settings " )
134- fallback. addButton ( withTitle: " Cancel " )
135- let result = fallback. runModal ( )
136- guard result == . alertFirstButtonReturn else { return }
137-
138- if let url = URL ( string: " x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility " ) {
139- NSWorkspace . shared. open ( url)
140- }
141- }
142-
143- // MARK: - Event tap lifecycle
144-
145- private func ensureAccessibilityAndStart( ) {
146- guard DockAccessibilityHelper . isAXEnabled ( ) else {
147- return
148- }
149- guard !hasStartedEventTap else {
150- return
151- }
152- hasStartedEventTap = true
153-
154- statusController. rebuildMenu ( )
155-
156- eventTapManager. start (
157- resolveAction: { [ weak self] flags in
158- guard let self else { return nil }
159-
160- // Choose a single effective modifier (priority: ⌘ > ⌥ > ⌃ > ⇧ > none)
161- func effectiveModifier( from flags: NSEvent . ModifierFlags ) -> Preferences . ActivationChoice {
162- if flags. contains ( . command) { return . command }
163- if flags. contains ( . option) { return . option }
164- if flags. contains ( . control) { return . control }
165- if flags. contains ( . shift) { return . shift }
166- return . none
167- }
168-
169- let eff = effectiveModifier ( from: flags)
170- let quitChoice = self . preferences. quitActivation
171- let forceChoice = self . preferences. forceActivation
172-
173- // If both are set to the same non-disabled choice, do nothing until resolved.
174- if quitChoice == forceChoice, quitChoice != . disabled {
175- return nil
176- }
177-
178- if eff == quitChoice, quitChoice != . disabled {
179- return . quit
180- }
181- if eff == forceChoice, forceChoice != . disabled {
182- return . forceQuit
183- }
184- return nil
185- } ,
186- handler: { [ weak self] point, action in
187- guard let self else { return false }
188- if let pid = self . dockHelper. pidForDockTile ( at: point) {
189- switch action {
190- case . quit:
191- self . quitController. gracefulQuit ( pid: pid)
192- case . forceQuit:
193- self . quitController. forceQuit ( pid: pid)
194- }
195- return self . eventTapManager. canSwallow
196- }
197- return false
198- }
199- )
200- }
201-
202- func applyStatusItemVisibility( show: Bool ) {
203- if show {
204- statusController. show ( )
205- NSApp . setActivationPolicy ( . accessory) // menu bar only
206- } else {
207- statusController. hide ( )
208- NSApp . setActivationPolicy ( . prohibited) // fully background
209- }
210- }
211-
212- func applicationWillTerminate( _ notification: Notification ) {
213- axPollingTimer? . invalidate ( )
214- eventTapManager. stop ( )
215- }
216- }
217-
34+ // ... rest unchanged ...
0 commit comments