@@ -49,7 +49,8 @@ class NotchOverlayController: NSObject {
4949 private var mouseTrackingTimer : AnyCancellable ?
5050 private var cursorTrackingTimer : AnyCancellable ?
5151 private var currentScreenID : UInt32 = 0
52- private var statusItem : NSStatusItem ?
52+ private var stopButtonPanel : NSPanel ?
53+ private var escMonitor : Any ?
5354
5455 func show( text: String , hasNextPage: Bool = false , onComplete: ( ( ) -> Void ) ? = nil ) {
5556 self . onComplete = onComplete
@@ -80,17 +81,33 @@ class NotchOverlayController: NSObject {
8081
8182 let screenFrame = screen. frame
8283
83- if settings. overlayMode == . floating && settings. followCursorWhenUndocked {
84+ if settings. overlayMode == . fullscreen {
85+ let fsScreen : NSScreen
86+ if settings. fullscreenScreenID != 0 ,
87+ let match = NSScreen . screens. first ( where: { $0. displayID == settings. fullscreenScreenID } ) {
88+ fsScreen = match
89+ } else {
90+ fsScreen = screen
91+ }
92+ showFullscreen ( settings: settings, screen: fsScreen)
93+ } else if settings. overlayMode == . floating && settings. followCursorWhenUndocked {
8494 showFollowCursor ( settings: settings, screen: screen)
8595 } else {
8696 switch settings. overlayMode {
8797 case . pinned:
8898 showPinned ( settings: settings, screen: screen)
8999 case . floating:
90100 showFloating ( settings: settings, screenFrame: screenFrame)
101+ case . fullscreen:
102+ break // handled above
91103 }
92104 }
93105
106+ // Show floating stop button only in follow-cursor mode (panel ignores mouse events)
107+ if settings. overlayMode == . floating && settings. followCursorWhenUndocked {
108+ showStopButton ( on: screen)
109+ }
110+
94111 // Word tracking & silence-paused need the microphone; classic does not
95112 if settings. listeningMode != . classic {
96113 speechRecognizer. start ( with: text)
@@ -157,8 +174,17 @@ class NotchOverlayController: NSObject {
157174 let cursorOffset : CGFloat = 8
158175 let x = mouse. x + cursorOffset
159176 let h = panel. frame. height
160- let y = mouse. y - h
177+ var y = mouse. y - h
161178 let w = panel. frame. width
179+
180+ // Keep panel below the menu bar so the status bar stop button stays visible
181+ if let screen = NSScreen . screens. first ( where: { NSMouseInRect ( mouse, $0. frame, false ) } ) {
182+ let menuBarBottom = screen. visibleFrame. maxY
183+ if y + h > menuBarBottom {
184+ y = menuBarBottom - h
185+ }
186+ }
187+
162188 panel. setFrame ( NSRect ( x: x, y: y, width: w, height: h) , display: false )
163189 }
164190
@@ -272,7 +298,44 @@ class NotchOverlayController: NSObject {
272298 self . panel = panel
273299
274300 startCursorTracking ( )
275- showStatusItem ( )
301+ }
302+
303+ private func showFullscreen( settings: NotchSettings , screen: NSScreen ) {
304+ let screenFrame = screen. frame
305+
306+ let fullscreenView = ExternalDisplayView (
307+ content: overlayContent,
308+ speechRecognizer: speechRecognizer,
309+ mirrorAxis: nil
310+ )
311+ let contentView = NSHostingView ( rootView: fullscreenView)
312+
313+ let panel = NSPanel (
314+ contentRect: screenFrame,
315+ styleMask: [ . borderless, . nonactivatingPanel] ,
316+ backing: . buffered,
317+ defer: false
318+ )
319+ panel. isOpaque = true
320+ panel. backgroundColor = . black
321+ panel. hasShadow = false
322+ panel. level = . screenSaver
323+ panel. collectionBehavior = [ . canJoinAllSpaces, . fullScreenAuxiliary]
324+ panel. ignoresMouseEvents = false
325+ panel. sharingType = settings. hideFromScreenShare ? . none : . readOnly
326+ panel. contentView = contentView
327+ panel. setFrame ( screenFrame, display: true )
328+ panel. orderFrontRegardless ( )
329+ self . panel = panel
330+
331+ // ESC key to stop the prompter
332+ escMonitor = NSEvent . addLocalMonitorForEvents ( matching: . keyDown) { [ weak self] event in
333+ if event. keyCode == 53 { // ESC
334+ self ? . dismiss ( )
335+ return nil
336+ }
337+ return event
338+ }
276339 }
277340
278341 private func showFloating( settings: NotchSettings , screenFrame: CGRect ) {
@@ -318,7 +381,8 @@ class NotchOverlayController: NSObject {
318381 DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.4 ) { [ weak self] in
319382 self ? . stopMouseTracking ( )
320383 self ? . stopCursorTracking ( )
321- self ? . removeStatusItem ( )
384+ self ? . removeStopButton ( )
385+ self ? . removeEscMonitor ( )
322386 self ? . panel? . orderOut ( nil )
323387 self ? . panel = nil
324388 self ? . frameTracker = nil
@@ -330,7 +394,8 @@ class NotchOverlayController: NSObject {
330394 private func forceClose( ) {
331395 stopMouseTracking ( )
332396 stopCursorTracking ( )
333- removeStatusItem ( )
397+ removeStopButton ( )
398+ removeEscMonitor ( )
334399 cancellables. removeAll ( )
335400 speechRecognizer. forceStop ( )
336401 speechRecognizer. recognizedCharCount = 0
@@ -362,7 +427,8 @@ class NotchOverlayController: NSObject {
362427 DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.4 ) {
363428 self . stopMouseTracking ( )
364429 self . stopCursorTracking ( )
365- self . removeStatusItem ( )
430+ self . removeStopButton ( )
431+ self . removeEscMonitor ( )
366432 self . cancellables. removeAll ( )
367433 self . panel? . orderOut ( nil )
368434 self . panel = nil
@@ -378,27 +444,69 @@ class NotchOverlayController: NSObject {
378444 panel != nil
379445 }
380446
381- // MARK: - Status Bar Item (for follow-cursor mode)
447+ // MARK: - Floating Stop Button
448+
449+ private func showStopButton( on screen: NSScreen ) {
450+ guard stopButtonPanel == nil else { return }
451+
452+ let buttonSize : CGFloat = 36
453+ let margin : CGFloat = 8
454+ let screenFrame = screen. frame
455+ let visibleFrame = screen. visibleFrame
456+ let menuBarBottom = visibleFrame. maxY
457+ let x = screenFrame. midX - buttonSize / 2
458+ let y = menuBarBottom - buttonSize - margin
459+
460+ let stopView = NSHostingView ( rootView: StopButtonView {
461+ self . dismiss ( )
462+ } )
463+
464+ let panel = NSPanel (
465+ contentRect: NSRect ( x: x, y: y, width: buttonSize, height: buttonSize) ,
466+ styleMask: [ . borderless, . nonactivatingPanel] ,
467+ backing: . buffered,
468+ defer: false
469+ )
470+ panel. isOpaque = false
471+ panel. backgroundColor = . clear
472+ panel. hasShadow = true
473+ panel. level = . screenSaver
474+ panel. collectionBehavior = [ . canJoinAllSpaces, . fullScreenAuxiliary]
475+ panel. ignoresMouseEvents = false
476+ panel. sharingType = . none
477+ panel. contentView = stopView
478+ panel. orderFrontRegardless ( )
479+ stopButtonPanel = panel
480+ }
382481
383- private func showStatusItem( ) {
384- guard statusItem == nil else { return }
385- print ( " [Textream] Showing status bar item for follow-cursor mode " )
386- let item = NSStatusBar . system. statusItem ( withLength: NSStatusItem . variableLength)
387- item. button? . title = " ■ Stop Prompter "
388- item. button? . target = self
389- item. button? . action = #selector( statusItemStop)
390- statusItem = item
482+ private func removeStopButton( ) {
483+ stopButtonPanel? . orderOut ( nil )
484+ stopButtonPanel = nil
391485 }
392486
393- private func removeStatusItem ( ) {
394- if let statusItem {
395- NSStatusBar . system . removeStatusItem ( statusItem )
487+ private func removeEscMonitor ( ) {
488+ if let escMonitor {
489+ NSEvent . removeMonitor ( escMonitor )
396490 }
397- statusItem = nil
491+ escMonitor = nil
398492 }
493+ }
494+
495+ // MARK: - Floating Stop Button View
399496
400- @objc private func statusItemStop( ) {
401- dismiss ( )
497+ struct StopButtonView : View {
498+ let onStop : ( ) -> Void
499+
500+ var body : some View {
501+ Button ( action: onStop) {
502+ Image ( systemName: " stop.fill " )
503+ . font ( . system( size: 14 , weight: . bold) )
504+ . foregroundStyle ( . white)
505+ . frame ( width: 36 , height: 36 )
506+ . background ( . red. opacity ( 0.85 ) )
507+ . clipShape ( Circle ( ) )
508+ }
509+ . buttonStyle ( . plain)
402510 }
403511}
404512
0 commit comments