Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

263 lines
9.4 KiB
Swift

import AppKit
import ApplicationServices
import CoreGraphics
import MacToolsPluginKit
// MARK: - Errors
enum MenuBarHiddenEventError: Error {
case notTrusted
case eventCreationFailed
case pidDead(pid_t)
func localizedDescription(localization: PluginLocalization) -> String {
switch self {
case .notTrusted:
localization.string("eventError.notTrusted", defaultValue: "辅助功能权限未授予")
case .eventCreationFailed:
localization.string("eventError.eventCreationFailed", defaultValue: "无法创建鼠标事件")
case .pidDead(let pid):
localization.format("eventError.pidDead", defaultValue: "目标进程 %d 已退出", pid)
}
}
}
// MARK: - MenuBarHiddenEventSynthesis
//
// Synthesises the CGEvents required to:
// * Drag a menu bar item (Cmd+drag is the gesture macOS recognises).
// * Click a menu bar item (left or right) so its menu opens.
//
// Accessibility permission (`AXIsProcessTrusted`) is mandatory for both.
@MainActor
final class MenuBarHiddenEventSynthesis {
/// Cmd+drag as Thaw does it: send a targeted mouseDown to the destination
/// edge, then a targeted mouseUp. Window Server treats this as moving the
/// menu-bar item's window next to that edge.
func move(item: MenuBarItem, to target: MenuBarHiddenResolvedMoveTarget) async throws {
guard AXIsProcessTrusted() else { throw MenuBarHiddenEventError.notTrusted }
let pid = eventPID(for: item)
guard kill(pid, 0) == 0 else { throw MenuBarHiddenEventError.pidDead(pid) }
let source = try makeEventSource()
guard
let down = CGEvent(
mouseEventSource: source,
mouseType: .leftMouseDown,
mouseCursorPosition: target.point,
mouseButton: .left
),
let up = CGEvent(
mouseEventSource: source,
mouseType: .leftMouseUp,
mouseCursorPosition: target.point,
mouseButton: .left
)
else { throw MenuBarHiddenEventError.eventCreationFailed }
down.flags = .maskCommand
down.setMenuBarItemWindowID(item.windowID, includeWindowID: true)
up.setMenuBarItemWindowID(target.windowID ?? item.windowID, includeWindowID: true)
let savedCursor = MouseCursorHelper.locationCoreGraphics
let shouldWarpToTarget = MouseCursorHelper.isOnActiveDisplay(target.point)
if shouldWarpToTarget {
MouseCursorHelper.warpCursor(to: target.point)
}
MouseCursorHelper.hideCursor()
defer {
if let savedCursor {
MouseCursorHelper.warpCursor(to: savedCursor)
}
MouseCursorHelper.showCursor()
}
if shouldWarpToTarget {
try await Task.sleep(for: .milliseconds(20))
}
down.post(tap: .cghidEventTap)
down.postToPid(pid)
try await Task.sleep(for: .milliseconds(50))
up.post(tap: .cghidEventTap)
up.postToPid(pid)
// Double mouseUp prevents stuck drag state.
up.post(tap: .cghidEventTap)
up.postToPid(pid)
try await Task.sleep(for: .milliseconds(50))
}
/// Sends a left or right click to a menu bar item at its current bounds.
func click(item: MenuBarItem, button: CGMouseButton) async throws {
guard AXIsProcessTrusted() else { throw MenuBarHiddenEventError.notTrusted }
let pid = eventPID(for: item)
guard kill(pid, 0) == 0 else { throw MenuBarHiddenEventError.pidDead(pid) }
let source = try makeEventSource()
let clickBounds = MenuBarHiddenWindowServer.screenRect(for: item.windowID) ?? item.bounds
let clickPoint = clickBounds.center
let (downType, upType): (CGEventType, CGEventType) = button == .left
? (.leftMouseDown, .leftMouseUp)
: (.rightMouseDown, .rightMouseUp)
guard
let down = CGEvent(
mouseEventSource: source,
mouseType: downType,
mouseCursorPosition: clickPoint,
mouseButton: button
),
let up = CGEvent(
mouseEventSource: source,
mouseType: upType,
mouseCursorPosition: clickPoint,
mouseButton: button
)
else { throw MenuBarHiddenEventError.eventCreationFailed }
down.setMenuBarItemWindowID(item.windowID, includeWindowID: false)
up.setMenuBarItemWindowID(item.windowID, includeWindowID: false)
down.setIntegerValueField(.mouseEventClickState, value: 1)
up.setIntegerValueField(.mouseEventClickState, value: 1)
let savedCursor = MouseCursorHelper.locationCoreGraphics
MouseCursorHelper.warpCursor(to: clickPoint)
try await Task.sleep(for: .milliseconds(10))
MouseCursorHelper.hideCursor()
defer {
if let savedCursor {
MouseCursorHelper.warpCursor(to: savedCursor)
}
MouseCursorHelper.showCursor()
}
down.post(tap: .cghidEventTap)
down.postToPid(pid)
try await Task.sleep(for: .milliseconds(30))
up.post(tap: .cghidEventTap)
up.postToPid(pid)
up.post(tap: .cghidEventTap)
up.postToPid(pid)
try await Task.sleep(for: .milliseconds(20))
}
/// Brief poll until the user's cursor sits still — avoids fighting active
/// user input when synthesising drag events.
func waitForUserInputPause() async {
var stable = 0
var previous = NSEvent.mouseLocation
for _ in 0 ..< 20 {
let current = NSEvent.mouseLocation
if hypot(current.x - previous.x, current.y - previous.y) < 2 {
stable += 1
if stable >= 3 { return }
} else {
stable = 0
}
previous = current
try? await Task.sleep(for: .milliseconds(30))
}
}
// MARK: - Private
private func eventPID(for item: MenuBarItem) -> pid_t {
item.sourcePID ?? item.ownerPID
}
private func makeEventSource() throws -> CGEventSource {
guard let source = CGEventSource(stateID: .hidSystemState) else {
throw MenuBarHiddenEventError.eventCreationFailed
}
source.setLocalEventsFilterDuringSuppressionState(
CGEventFilterMask(rawValue: 0x7),
state: .eventSuppressionStateRemoteMouseDrag
)
source.setLocalEventsFilterDuringSuppressionState(
CGEventFilterMask(rawValue: 0x7),
state: .eventSuppressionStateSuppressionInterval
)
source.localEventsSuppressionInterval = 0
return source
}
}
// MARK: - Helpers
enum MouseCursorHelper {
static var locationCoreGraphics: CGPoint? {
CGEvent(source: nil)?.location
}
static func warpCursor(to point: CGPoint) {
let result = CGWarpMouseCursorPosition(point)
guard result != .success,
let source = CGEventSource(stateID: .hidSystemState),
let event = CGEvent(
mouseEventSource: source,
mouseType: .mouseMoved,
mouseCursorPosition: point,
mouseButton: .left
)
else {
return
}
event.post(tap: .cghidEventTap)
}
static func hideCursor() { CGDisplayHideCursor(kCGNullDirectDisplay) }
static func showCursor() { CGDisplayShowCursor(kCGNullDirectDisplay) }
static func isOnActiveDisplay(_ point: CGPoint) -> Bool {
var count: UInt32 = 0
guard CGGetActiveDisplayList(0, nil, &count) == .success else { return false }
var displays = [CGDirectDisplayID](repeating: 0, count: Int(count))
guard CGGetActiveDisplayList(count, &displays, nil) == .success else { return false }
return displays.contains { CGDisplayBounds($0).contains(point) }
}
static func isButtonPressed() -> Bool {
for rawButton in 0 ... 31 {
guard let button = CGMouseButton(rawValue: UInt32(rawButton)) else { continue }
if CGEventSource.buttonState(.combinedSessionState, button: button) {
return true
}
}
return false
}
static func lastMovementOccurred(within duration: TimeInterval) -> Bool {
let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: .mouseMoved)
return seconds <= duration
}
static func lastScrollWheelOccurred(within duration: TimeInterval) -> Bool {
let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: .scrollWheel)
return seconds <= duration
}
}
extension CGRect {
var center: CGPoint { CGPoint(x: midX, y: midY) }
}
private extension CGEventField {
static let menuBarHiddenWindowID = CGEventField(rawValue: 0x33)!
}
private extension CGEvent {
func setMenuBarItemWindowID(_ windowID: CGWindowID, includeWindowID: Bool) {
let value = Int64(windowID)
setIntegerValueField(.eventSourceUserData, value: Int64(Int(bitPattern: ObjectIdentifier(self))))
setIntegerValueField(.mouseEventWindowUnderMousePointer, value: value)
setIntegerValueField(.mouseEventWindowUnderMousePointerThatCanHandleThisEvent, value: value)
if includeWindowID {
setIntegerValueField(.menuBarHiddenWindowID, value: value)
}
}
}