Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to prevent other applications from listening to mouse events in macOS?

I am making a macOS app in Swift and the window of my app is always on top of other apps. This is working fine with all apps even if those apps are in full-screen mode. However, when Keynote runs in full-screen mode and my app is on top of it, all the mouse events meant for my app also go to keynote and it exits fullscreen.

I am not sure what's going on but I need to prevent keynote from exiting full-screen. How can I do that?

The window level of my app is NSWindow.Level.popUpMenu. And, I have tried the following but nothing has worked so far:

window.orderFrontRegardless()
window.makeKeyAndOrderFront(self)
window.order(.above, relativeTo: 0)
like image 553
Ram Patra Avatar asked Sep 19 '25 04:09

Ram Patra


1 Answers

TL;DR - The problem here is the application activation.


This doesn't answer your question precisely:

How to prevent other applications from listening to mouse events in macOS?

It's an answer demonstrating how to achieve what you want without preventing other applications from listening to mouse events in macOS.


Active vs inactive window

Check the following screenshots. The first one contains active Xcode window and the other one inactive Xcode window. Your goal is to keep the other application window active even if you click in your overlay. It's irrelevant if the other application is running presentation (like Keynote, in full screen) or not.

Active Xcode window enter image description here

Sample project setup

  • Create a new project (Xcode - macOS App - Swift & Storyboard)
  • Open the Main.storyboard and remove window & view controller scenes
  • Set LSUIElement to YES (Info.plist)
  • Add the HotKey Swift package (https://github.com/soffes/HotKey)
  • Copy & paste the AppDelegate.swift code (below)
  • Run it
  • Toggle the red overlay with Cmd + Opt + O

I just tested it with the Keynote 10.0 & macOS Catalina 10.15.4 (19E287) and it works as expected - I can click inside the red overlay without interrupting the running presentation, I can control the presentation with keyboard, ...

Important parts

  • Use NSPanel instead of NSWindow
  • Use styleMask & .nonactivatingPanel (can't be used with NSWindow)
    • Do not activate -> do not deactivate others
  • Set hidesOnDeactivate to false
    • Do not hide when you launch your app, is activated and then you activate any other app
  • Set becomesKeyOnlyIfNeeded to true
    • Avoid being the key window with mouse clicks
    • Search for the needsPanelToBecomeKey if you need keyboard input
  • Set collectionBehavior to [.canJoinAllSpaces, .fullScreenAuxiliary]
    • .canJoinAllSpaces = the window appears in all spaces (like menu bar)
    • .fullScreenAuxiliary = the window with this collection behavior can be shown on the same space as the fullscreen window

AppDelegate.swift

import Cocoa
import HotKey

final class OverlayView: NSView {
    private var path: NSBezierPath?

    override func keyDown(with event: NSEvent) {
        print("keyDown - \(event.keyCode)")
    }

    override func keyUp(with event: NSEvent) {
        print("keyUp - \(event.keyCode)")
    }

    override func mouseDown(with event: NSEvent) {
        let point = self.convert(event.locationInWindow, from: nil)

        path = NSBezierPath()
        path?.move(to: point)
        needsDisplay = true
    }

    override func mouseUp(with event: NSEvent) {
        path = nil
        needsDisplay = true
    }

    override func mouseDragged(with event: NSEvent) {
        let point = self.convert(event.locationInWindow, from: nil)
        path?.line(to: point)
        needsDisplay = true
    }

    override func draw(_ dirtyRect: NSRect) {
        guard let ctx = NSGraphicsContext.current?.cgContext else {
            return
        }

        defer {
            ctx.restoreGState()
        }
        ctx.saveGState()

        NSColor.green.set()
        ctx.stroke(bounds, width: 8.0)

        guard let path = path else {
            return
        }

        path.lineWidth = 5.0
        NSColor.green.set()
        path.stroke()
    }

    override var acceptsFirstResponder: Bool {
        true
    }

    override var needsPanelToBecomeKey: Bool {
        true
    }
}

final class OverlayWindow: NSPanel {
    convenience init() {
        self.init(
            contentRect: NSScreen.main!.frame,
            styleMask: [.borderless, .fullSizeContentView, .nonactivatingPanel],
            backing: .buffered,
            defer: false
        )

        canHide = false
        hidesOnDeactivate = false
        contentView = OverlayView()
        isFloatingPanel = true
        becomesKeyOnlyIfNeeded = true
        acceptsMouseMovedEvents = true
        isOpaque = false
        hasShadow = false
        titleVisibility = .hidden
        level = .popUpMenu
        backgroundColor = NSColor.black.withAlphaComponent(0.001)
        collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
    }

    override var canBecomeKey: Bool {
        true
    }
}

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    private var hotKey: HotKey!
    private var overlayWindowController: NSWindowController?

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        hotKey = HotKey(key: .o, modifiers: [.command, .option])
        hotKey.keyDownHandler = toggleOverlay
    }

    private func toggleOverlay() {
        if overlayWindowController != nil {
            overlayWindowController?.close()
            overlayWindowController = nil
        } else {
            overlayWindowController = NSWindowController(window: OverlayWindow())
            overlayWindowController?.showWindow(self)
            overlayWindowController?.window?.makeKey()
        }
    }

    func applicationWillTerminate(_ aNotification: Notification) {
    }
}
like image 183
zrzka Avatar answered Sep 22 '25 02:09

zrzka