@@ -20,11 +20,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
2020 // window delegates must be held strongly (NSWindow.delegate is weak)
2121 private var prefsWindowDelegate : WindowCloseDelegate ?
2222 private var aboutWindowDelegate : WindowCloseDelegate ?
23+ private var pauseWindowDelegate : WindowCloseDelegate ?
2324
2425 func applicationDidFinishLaunching( _ notification: Notification ) {
2526 NSApp . setActivationPolicy ( . accessory)
2627
27- // force initialization so schedule evaluation starts immediately
2828 _ = ScheduleManager . shared
2929 _ = BlocklistManager . shared
3030
@@ -33,8 +33,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3333
3434 NotificationCenter . default. addObserver (
3535 self ,
36- selector: #selector( showPauseConfirmation ) ,
37- name: . tomeShowPauseConfirmation ,
36+ selector: #selector( openPause ) ,
37+ name: . tomeOpenPauseWindow ,
3838 object: nil
3939 )
4040 }
@@ -81,41 +81,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
8181 let m = NSMenu ( )
8282
8383 // Status line
84- let statusItem = NSMenuItem ( title: statusTitle ( ) , action: nil , keyEquivalent: " " )
85- statusItem . isEnabled = false
86- m. addItem ( statusItem )
84+ let statusMenuItem = NSMenuItem ( title: statusTitle ( ) , action: nil , keyEquivalent: " " )
85+ statusMenuItem . isEnabled = false
86+ m. addItem ( statusMenuItem )
8787
8888 m. addItem ( . separator( ) )
8989
90- // Pause / Cancel Pause / Resume — always visible, context-sensitive
91- if appState. isPaused {
92- let breakRemaining = pauseManager. breakSecondsRemaining
93- let mins = breakRemaining / 60
94- let secs = breakRemaining % 60
95- let resumeItem = NSMenuItem (
96- title: String ( format: " End break early (%d:%02d left) " , mins, secs) ,
97- action: #selector( endBreakEarly) ,
98- keyEquivalent: " "
99- )
100- resumeItem. target = self
101- m. addItem ( resumeItem)
102- } else if appState. pauseRequestActive {
103- let remaining = pauseManager. countdownSecondsRemaining
104- let mins = remaining / 60
105- let secs = remaining % 60
106- let cancelItem = NSMenuItem (
107- title: String ( format: " Cancel pause request (%d:%02d) " , mins, secs) ,
108- action: #selector( cancelPauseRequest) ,
109- keyEquivalent: " "
110- )
111- cancelItem. target = self
112- m. addItem ( cancelItem)
113- } else {
114- let pauseItem = NSMenuItem ( title: " Request pause... " , action: #selector( requestPause) , keyEquivalent: " " )
115- pauseItem. target = self
116- pauseItem. isEnabled = appState. isActivelyBlocking
117- m. addItem ( pauseItem)
118- }
90+ // Pause — always present, grayed out when not applicable
91+ let pauseItem = NSMenuItem ( title: " Pause... " , action: #selector( openPause) , keyEquivalent: " " )
92+ pauseItem. target = self
93+ pauseItem. isEnabled = appState. pauseWindowEnabled
94+ m. addItem ( pauseItem)
11995
12096 m. addItem ( . separator( ) )
12197
@@ -132,12 +108,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
132108 m. addItem ( . separator( ) )
133109
134110 // Quit — disabled in locked mode during active blocking
135- let quitItem = NSMenuItem ( title: " Quit Tome " , action: #selector( quit) , keyEquivalent: " q " )
111+ let quitItem = NSMenuItem (
112+ title: appState. lockedMode && appState. isActivelyBlocking ? " Quit Tome (locked) " : " Quit Tome " ,
113+ action: #selector( quit) ,
114+ keyEquivalent: " q "
115+ )
136116 quitItem. target = self
137- if appState. lockedMode && appState. isActivelyBlocking {
138- quitItem. isEnabled = false
139- quitItem. title = " Quit Tome (locked) "
140- }
117+ quitItem. isEnabled = !( appState. lockedMode && appState. isActivelyBlocking)
141118 m. addItem ( quitItem)
142119
143120 self . menu = m
@@ -146,9 +123,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
146123
147124 private func statusTitle( ) -> String {
148125 if appState. isPaused {
149- let mins = pauseManager. breakSecondsRemaining / 60
150- let secs = pauseManager. breakSecondsRemaining % 60
151- return String ( format: " Paused — %d:%02d remaining " , mins, secs)
126+ let remaining = pauseManager. breakSecondsRemaining
127+ return String ( format: " Paused — %d:%02d remaining " , remaining / 60 , remaining % 60 )
152128 } else if appState. isBlocking {
153129 if let first = appState. activeSchedules. first {
154130 return " Blocking · \( first. name) "
@@ -162,38 +138,34 @@ class AppDelegate: NSObject, NSApplicationDelegate {
162138 // MARK: - Observers
163139
164140 private func setupObservers( ) {
165- appState. $isBlocking
166- . receive ( on: RunLoop . main)
167- . sink { [ weak self] _ in self ? . updateStatusIcon ( ) }
168- . store ( in: & cancellables)
169-
170- appState. $isPaused
171- . receive ( on: RunLoop . main)
172- . sink { [ weak self] _ in self ? . updateStatusIcon ( ) }
173- . store ( in: & cancellables)
174- }
175-
176- // MARK: - Actions
177-
178- @objc private func requestPause( ) {
179- pauseManager. requestPause ( )
180- rebuildMenu ( )
181- }
141+ // update icon + rebuild menu on any relevant state change
142+ let rebuild : ( Any ) -> Void = { [ weak self] _ in
143+ DispatchQueue . main. async {
144+ self ? . updateStatusIcon ( )
145+ self ? . rebuildMenu ( )
146+ }
147+ }
182148
183- @objc private func cancelPauseRequest( ) {
184- pauseManager. cancelPauseRequest ( )
185- rebuildMenu ( )
149+ appState. $isBlocking. receive ( on: RunLoop . main) . sink ( receiveValue: rebuild) . store ( in: & cancellables)
150+ appState. $isPaused. receive ( on: RunLoop . main) . sink ( receiveValue: rebuild) . store ( in: & cancellables)
151+ appState. $pauseRequestActive. receive ( on: RunLoop . main) . sink ( receiveValue: rebuild) . store ( in: & cancellables)
152+ appState. $pendingPauseConfirmation. receive ( on: RunLoop . main) . sink ( receiveValue: rebuild) . store ( in: & cancellables)
186153 }
187154
188- @objc private func endBreakEarly( ) {
189- pauseManager. endPause ( )
190- rebuildMenu ( )
191- }
155+ // MARK: - Actions
192156
193- @objc func showPauseConfirmation( ) {
194- let view = PauseConfirmView ( )
195- . environmentObject ( appState)
196- showFloatingWindow ( title: " Confirm Pause " , content: view, identifier: " pause " , size: NSSize ( width: 320 , height: 240 ) )
157+ @objc func openPause( ) {
158+ if pauseWindow == nil {
159+ let view = PauseView ( ) . environmentObject ( appState)
160+ let window = makeWindow ( title: " Pause " , content: view, size: NSSize ( width: 320 , height: 300 ) )
161+ window. styleMask = [ . titled, . closable]
162+ pauseWindow = window
163+ let delegate = WindowCloseDelegate { [ weak self] in self ? . pauseWindow = nil }
164+ pauseWindowDelegate = delegate
165+ window. delegate = delegate
166+ }
167+ pauseWindow? . makeKeyAndOrderFront ( nil )
168+ NSApp . activate ( ignoringOtherApps: true )
197169 }
198170
199171 @objc private func openPreferences( ) {
@@ -204,9 +176,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
204176 . environmentObject ( ScheduleManager . shared)
205177 let window = makeWindow ( title: " Tome Preferences " , content: view, size: NSSize ( width: 680 , height: 520 ) )
206178 prefsWindow = window
207- let prefsDelegate = WindowCloseDelegate { [ weak self] in self ? . prefsWindow = nil }
208- prefsWindowDelegate = prefsDelegate
209- window. delegate = prefsDelegate
179+ let delegate = WindowCloseDelegate { [ weak self] in self ? . prefsWindow = nil }
180+ prefsWindowDelegate = delegate
181+ window. delegate = delegate
210182 }
211183 prefsWindow? . makeKeyAndOrderFront ( nil )
212184 NSApp . activate ( ignoringOtherApps: true )
@@ -218,9 +190,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
218190 let window = makeWindow ( title: " About Tome " , content: view, size: NSSize ( width: 340 , height: 300 ) )
219191 window. styleMask = [ . titled, . closable]
220192 aboutWindow = window
221- let aboutDelegate = WindowCloseDelegate { [ weak self] in self ? . aboutWindow = nil }
222- aboutWindowDelegate = aboutDelegate
223- window. delegate = aboutDelegate
193+ let delegate = WindowCloseDelegate { [ weak self] in self ? . aboutWindow = nil }
194+ aboutWindowDelegate = delegate
195+ window. delegate = delegate
224196 }
225197 aboutWindow? . makeKeyAndOrderFront ( nil )
226198 NSApp . activate ( ignoringOtherApps: true )
@@ -245,23 +217,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
245217 window. isReleasedWhenClosed = false
246218 return window
247219 }
248-
249- private func showFloatingWindow< V: View > ( title: String , content: V , identifier: String , size: NSSize ) {
250- let window = NSPanel (
251- contentRect: NSRect ( origin: . zero, size: size) ,
252- styleMask: [ . titled, . closable, . nonactivatingPanel, . hudWindow] ,
253- backing: . buffered,
254- defer: false
255- )
256- window. title = title
257- window. center ( )
258- window. contentView = NSHostingView ( rootView: content)
259- window. isReleasedWhenClosed = true
260- window. level = . floating
261- window. makeKeyAndOrderFront ( nil )
262- NSApp . activate ( ignoringOtherApps: true )
263- pauseWindow = window
264- }
265220}
266221
267222class WindowCloseDelegate : NSObject , NSWindowDelegate {
0 commit comments