Add Click Monitor for ensuring consistent open on click

This commit is contained in:
Hariharan Mudaliar 2026-02-16 15:00:03 +05:30
commit 72d101b1be
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
3 changed files with 90 additions and 6 deletions

View file

@ -126,6 +126,8 @@ struct ContentView: View {
@State private var hoverTask: Task<Void, Never>? @State private var hoverTask: Task<Void, Never>?
@State private var isHovering: Bool = false @State private var isHovering: Bool = false
@State private var lastHapticTime: Date = Date() @State private var lastHapticTime: Date = Date()
@State private var clickMonitor: Any?
@State private var hostingWindow: NSWindow?
@State private var gestureProgress: CGFloat = .zero @State private var gestureProgress: CGFloat = .zero
@State private var skipGestureActiveDirection: MusicManager.SkipDirection? @State private var skipGestureActiveDirection: MusicManager.SkipDirection?
@ -271,6 +273,7 @@ struct ContentView: View {
} }
.conditionalModifier(interactionsEnabled) { view in .conditionalModifier(interactionsEnabled) { view in
view view
.contentShape(Rectangle())
.onHover { hovering in .onHover { hovering in
handleHover(hovering) handleHover(hovering)
} }
@ -389,12 +392,37 @@ struct ContentView: View {
// #endif // #endif
// .keyboardShortcut("E", modifiers: .command) // .keyboardShortcut("E", modifiers: .command)
} }
if interactionsEnabled && vm.notchState == .closed {
Color.clear
.frame(height: 8)
.frame(maxWidth: .infinity, alignment: .top)
.contentShape(Rectangle())
.onHover { hovering in
handleHover(hovering)
}
.onTapGesture {
if Defaults[.enableHaptics] {
triggerHapticIfAllowed()
}
openNotch()
}
}
} }
.frame( .background(WindowAccessor { window in
maxWidth: dynamicNotchSize.width, hostingWindow = window
maxHeight: dynamicNotchSize.height + currentShadowPadding, })
alignment: .top .onAppear {
) startGlobalClickMonitor()
}
.onDisappear {
stopGlobalClickMonitor()
}
.frame(
maxWidth: dynamicNotchSize.width,
maxHeight: dynamicNotchSize.height + currentShadowPadding,
alignment: .top
)
.environmentObject(privacyManager) .environmentObject(privacyManager)
.onChange(of: dynamicNotchSize) { oldSize, newSize in .onChange(of: dynamicNotchSize) { oldSize, newSize in
guard oldSize != newSize else { return } guard oldSize != newSize else { return }
@ -1430,6 +1458,37 @@ struct ContentView: View {
} }
} }
private func startGlobalClickMonitor() {
guard clickMonitor == nil else { return }
clickMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown]) { _ in
handleGlobalClick()
}
}
private func stopGlobalClickMonitor() {
if let clickMonitor {
NSEvent.removeMonitor(clickMonitor)
self.clickMonitor = nil
}
}
private func handleGlobalClick() {
guard !lockScreenManager.isLocked else { return }
guard vm.notchState == .closed else { return }
guard let hostingWindow else { return }
let cursorLocation = NSEvent.mouseLocation
let hitFrame = hostingWindow.frame.insetBy(dx: -2, dy: -2)
guard hitFrame.contains(cursorLocation) else { return }
DispatchQueue.main.async {
if Defaults[.enableHaptics] {
triggerHapticIfAllowed()
}
openNotch()
}
}
// MARK: - Hover Management // MARK: - Hover Management
/// Handle hover state changes with debouncing /// Handle hover state changes with debouncing
@ -1992,6 +2051,24 @@ struct ContentView: View {
} }
} }
private struct WindowAccessor: NSViewRepresentable {
let callback: (NSWindow?) -> Void
func makeNSView(context: Context) -> NSView {
let view = NSView(frame: .zero)
DispatchQueue.main.async { [weak view] in
callback(view?.window)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
DispatchQueue.main.async { [weak nsView] in
callback(nsView?.window)
}
}
}
private enum MusicSecondaryLiveActivity: Equatable { private enum MusicSecondaryLiveActivity: Equatable {
case timer case timer
case reminder(ReminderLiveActivityManager.ReminderEntry) case reminder(ReminderLiveActivityManager.ReminderEntry)

View file

@ -79,6 +79,12 @@ struct DynamicNotchApp: App {
} }
} }
final class FirstMouseHostingView<Content: View>: NSHostingView<Content> {
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
true
}
}
extension AppDelegate { extension AppDelegate {
static var shared: AppDelegate? { static var shared: AppDelegate? {
NSApplication.shared.delegate as? AppDelegate NSApplication.shared.delegate as? AppDelegate
@ -223,7 +229,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
window.animationBehavior = .none window.animationBehavior = .none
window.contentView = NSHostingView( window.contentView = FirstMouseHostingView(
rootView: ContentView() rootView: ContentView()
.environmentObject(viewModel) .environmentObject(viewModel)
.environmentObject(webcamManager) .environmentObject(webcamManager)

View file

@ -64,4 +64,5 @@ class DynamicIslandWindow: NSPanel {
override var canBecomeMain: Bool { override var canBecomeMain: Bool {
true true
} }
} }