mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-17 14:07:28 +08:00
- Extract 0.1 second delay to named constant 'registrationDelay' with explanatory comment - Split error message into separate log lines for better readability Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>
319 lines
15 KiB
Swift
319 lines
15 KiB
Swift
import Cocoa
|
|
import AVFoundation
|
|
import FlutterMacOS
|
|
import desktop_multi_window
|
|
// import bitsdojo_window_macos
|
|
|
|
import desktop_drop
|
|
import device_info_plus
|
|
import flutter_custom_cursor
|
|
import package_info_plus
|
|
import path_provider_foundation
|
|
import screen_retriever
|
|
import sqflite
|
|
// import tray_manager
|
|
import uni_links_desktop
|
|
import url_launcher_macos
|
|
import wakelock_plus
|
|
import window_manager
|
|
import window_size
|
|
import texture_rgba_renderer
|
|
|
|
// Global state for relative mouse mode
|
|
// All properties and methods must be accessed on the main thread since they
|
|
// interact with NSEvent monitors, CoreGraphics APIs, and Flutter channels.
|
|
// Note: We avoid @MainActor to maintain macOS 10.14 compatibility.
|
|
class RelativeMouseState {
|
|
static let shared = RelativeMouseState()
|
|
|
|
var enabled = false
|
|
var eventMonitor: Any?
|
|
var deltaChannel: FlutterMethodChannel?
|
|
var accumulatedDeltaX: CGFloat = 0
|
|
var accumulatedDeltaY: CGFloat = 0
|
|
|
|
private init() {}
|
|
}
|
|
|
|
class MainFlutterWindow: NSWindow {
|
|
override func awakeFromNib() {
|
|
rustdesk_core_main();
|
|
let flutterViewController = FlutterViewController.init()
|
|
let windowFrame = self.frame
|
|
self.contentViewController = flutterViewController
|
|
self.setFrame(windowFrame, display: true)
|
|
// register self method handler
|
|
let registrar = flutterViewController.registrar(forPlugin: "RustDeskPlugin")
|
|
setMethodHandler(registrar: registrar)
|
|
|
|
RegisterGeneratedPlugins(registry: flutterViewController)
|
|
|
|
FlutterMultiWindowPlugin.setOnWindowCreatedCallback { controller in
|
|
// Register the plugin which you want access from other isolate.
|
|
// DesktopLifecyclePlugin.register(with: controller.registrar(forPlugin: "DesktopLifecyclePlugin"))
|
|
// Note: copy below from above RegisterGeneratedPlugins
|
|
self.setMethodHandler(registrar: controller.registrar(forPlugin: "RustDeskPlugin"))
|
|
DesktopDropPlugin.register(with: controller.registrar(forPlugin: "DesktopDropPlugin"))
|
|
DeviceInfoPlusMacosPlugin.register(with: controller.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
|
FlutterCustomCursorPlugin.register(with: controller.registrar(forPlugin: "FlutterCustomCursorPlugin"))
|
|
FPPPackageInfoPlusPlugin.register(with: controller.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
|
PathProviderPlugin.register(with: controller.registrar(forPlugin: "PathProviderPlugin"))
|
|
SqflitePlugin.register(with: controller.registrar(forPlugin: "SqflitePlugin"))
|
|
// TrayManagerPlugin.register(with: controller.registrar(forPlugin: "TrayManagerPlugin"))
|
|
UniLinksDesktopPlugin.register(with: controller.registrar(forPlugin: "UniLinksDesktopPlugin"))
|
|
UrlLauncherPlugin.register(with: controller.registrar(forPlugin: "UrlLauncherPlugin"))
|
|
WakelockPlusMacosPlugin.register(with: controller.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
|
WindowSizePlugin.register(with: controller.registrar(forPlugin: "WindowSizePlugin"))
|
|
TextureRgbaRendererPlugin.register(with: controller.registrar(forPlugin: "TextureRgbaRendererPlugin"))
|
|
}
|
|
|
|
super.awakeFromNib()
|
|
}
|
|
|
|
override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
|
|
super.order(place, relativeTo: otherWin)
|
|
hiddenWindowAtLaunch()
|
|
}
|
|
|
|
/// Override window theme.
|
|
public func setWindowInterfaceMode(window: NSWindow, themeName: String) {
|
|
window.appearance = NSAppearance(named: themeName == "light" ? .aqua : .darkAqua)
|
|
}
|
|
|
|
private func enableNativeRelativeMouseMode(channel: FlutterMethodChannel) -> Bool {
|
|
assert(Thread.isMainThread, "enableNativeRelativeMouseMode must be called on the main thread")
|
|
let state = RelativeMouseState.shared
|
|
if state.enabled {
|
|
// Already enabled: update the channel so this caller receives deltas.
|
|
state.deltaChannel = channel
|
|
return true
|
|
}
|
|
|
|
// Dissociate mouse from cursor position - this locks the cursor in place
|
|
// Do this FIRST before setting any state
|
|
let result = CGAssociateMouseAndMouseCursorPosition(0)
|
|
if result != CGError.success {
|
|
NSLog("[RustDesk] Failed to dissociate mouse from cursor position: %d", result.rawValue)
|
|
return false
|
|
}
|
|
|
|
// Only set state after CG call succeeds
|
|
state.deltaChannel = channel
|
|
state.accumulatedDeltaX = 0
|
|
state.accumulatedDeltaY = 0
|
|
|
|
// Add local event monitor to capture mouse delta.
|
|
// Note: Local event monitors are always called on the main thread,
|
|
// so accessing main-thread-only state is safe here.
|
|
state.eventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged, .otherMouseDragged]) { [weak state] event in
|
|
guard let state = state else { return event }
|
|
// Guard against race: mode may be disabled between weak capture and this check.
|
|
guard state.enabled else { return event }
|
|
let deltaX = event.deltaX
|
|
let deltaY = event.deltaY
|
|
|
|
if deltaX != 0 || deltaY != 0 {
|
|
// Accumulate delta (main thread only - NSEvent local monitors always run on main thread)
|
|
state.accumulatedDeltaX += deltaX
|
|
state.accumulatedDeltaY += deltaY
|
|
|
|
// Only send if we have integer movement
|
|
let intX = Int(state.accumulatedDeltaX)
|
|
let intY = Int(state.accumulatedDeltaY)
|
|
|
|
if intX != 0 || intY != 0 {
|
|
state.accumulatedDeltaX -= CGFloat(intX)
|
|
state.accumulatedDeltaY -= CGFloat(intY)
|
|
|
|
// Send delta to Flutter (already on main thread)
|
|
state.deltaChannel?.invokeMethod("onMouseDelta", arguments: ["dx": intX, "dy": intY])
|
|
}
|
|
}
|
|
|
|
return event
|
|
}
|
|
|
|
// Check if monitor was created successfully
|
|
if state.eventMonitor == nil {
|
|
NSLog("[RustDesk] Failed to create event monitor for relative mouse mode")
|
|
// Re-associate mouse since we failed
|
|
CGAssociateMouseAndMouseCursorPosition(1)
|
|
state.deltaChannel = nil
|
|
return false
|
|
}
|
|
|
|
// Set enabled LAST after everything succeeds
|
|
state.enabled = true
|
|
return true
|
|
}
|
|
|
|
private func disableNativeRelativeMouseMode() {
|
|
assert(Thread.isMainThread, "disableNativeRelativeMouseMode must be called on the main thread")
|
|
let state = RelativeMouseState.shared
|
|
if !state.enabled { return }
|
|
|
|
state.enabled = false
|
|
|
|
// Remove event monitor
|
|
if let monitor = state.eventMonitor {
|
|
NSEvent.removeMonitor(monitor)
|
|
state.eventMonitor = nil
|
|
}
|
|
|
|
state.deltaChannel = nil
|
|
state.accumulatedDeltaX = 0
|
|
state.accumulatedDeltaY = 0
|
|
|
|
// Re-associate mouse with cursor position (non-blocking with async retry)
|
|
let result = CGAssociateMouseAndMouseCursorPosition(1)
|
|
if result != CGError.success {
|
|
NSLog("[RustDesk] Failed to re-associate mouse with cursor position: %d, scheduling retry...", result.rawValue)
|
|
// Non-blocking retry after 50ms
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
|
let retryResult = CGAssociateMouseAndMouseCursorPosition(1)
|
|
if retryResult != CGError.success {
|
|
NSLog("[RustDesk] Retry failed to re-associate mouse: %d. Cursor may remain locked.", retryResult.rawValue)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func setMethodHandler(registrar: FlutterPluginRegistrar) {
|
|
let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/host", binaryMessenger: registrar.messenger)
|
|
channel.setMethodCallHandler({
|
|
(call, result) -> Void in
|
|
switch call.method {
|
|
case "setWindowTheme":
|
|
let arg = call.arguments as! [String: Any]
|
|
let themeName = arg["themeName"] as? String
|
|
guard let window = registrar.view?.window else {
|
|
result(nil)
|
|
return
|
|
}
|
|
self.setWindowInterfaceMode(window: window,themeName: themeName ?? "light")
|
|
result(nil)
|
|
break;
|
|
case "terminate":
|
|
NSApplication.shared.terminate(self)
|
|
result(nil)
|
|
case "canRecordAudio":
|
|
switch AVCaptureDevice.authorizationStatus(for: .audio) {
|
|
case .authorized:
|
|
result(1)
|
|
break
|
|
case .notDetermined:
|
|
result(0)
|
|
break
|
|
default:
|
|
result(-1)
|
|
break
|
|
}
|
|
case "requestRecordAudio":
|
|
// Request microphone access and trigger system registration
|
|
// On macOS 13+, apps only appear in System Settings > Privacy & Security > Microphone
|
|
// after they actually attempt to use the microphone, not just request permission.
|
|
// We create a brief capture session to ensure proper registration.
|
|
AVCaptureDevice.requestAccess(for: .audio, completionHandler: { granted in
|
|
if granted {
|
|
// Instantiate an audio capture session to trigger macOS registration
|
|
// This needs to run on main thread to ensure proper lifecycle
|
|
DispatchQueue.main.async {
|
|
if let audioDevice = AVCaptureDevice.default(for: .audio) {
|
|
do {
|
|
let audioInput = try AVCaptureDeviceInput(device: audioDevice)
|
|
let captureSession = AVCaptureSession()
|
|
captureSession.beginConfiguration()
|
|
if captureSession.canAddInput(audioInput) {
|
|
captureSession.addInput(audioInput)
|
|
}
|
|
captureSession.commitConfiguration()
|
|
// Start and immediately stop the session to trigger registration
|
|
captureSession.startRunning()
|
|
// Minimum delay required for macOS to register the app in System Settings
|
|
let registrationDelay: TimeInterval = 0.1
|
|
// Keep a strong reference and stop after the registration delay
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + registrationDelay) { [captureSession] in
|
|
captureSession.stopRunning()
|
|
}
|
|
} catch {
|
|
NSLog("[RustDesk] Failed to create audio capture session: %@", error.localizedDescription)
|
|
NSLog("[RustDesk] The app may not appear in System Settings > Privacy & Security > Microphone")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
DispatchQueue.main.async {
|
|
result(granted)
|
|
}
|
|
})
|
|
break
|
|
case "bumpMouse":
|
|
var dx = 0
|
|
var dy = 0
|
|
|
|
if let argMap = call.arguments as? [String: Any] {
|
|
dx = (argMap["dx"] as? Int) ?? 0
|
|
dy = (argMap["dy"] as? Int) ?? 0
|
|
}
|
|
else if let argList = call.arguments as? [Any] {
|
|
dx = argList.count >= 1 ? (argList[0] as? Int) ?? 0 : 0
|
|
dy = argList.count >= 2 ? (argList[1] as? Int) ?? 0 : 0
|
|
}
|
|
|
|
var mouseLoc: CGPoint
|
|
|
|
if let dummyEvent = CGEvent(source: nil) { // can this ever fail?
|
|
mouseLoc = dummyEvent.location
|
|
}
|
|
else if let screenFrame = NSScreen.screens.first?.frame {
|
|
// NeXTStep: Origin is lower-left of primary screen, positive is up
|
|
// Cocoa Core Graphics: Origin is upper-left of primary screen, positive is down
|
|
let nsMouseLoc = NSEvent.mouseLocation
|
|
|
|
mouseLoc = CGPoint(
|
|
x: nsMouseLoc.x,
|
|
y: NSHeight(screenFrame) - nsMouseLoc.y)
|
|
}
|
|
else {
|
|
result(false)
|
|
break
|
|
}
|
|
|
|
let newLoc = CGPoint(x: mouseLoc.x + CGFloat(dx), y: mouseLoc.y + CGFloat(dy))
|
|
|
|
CGDisplayMoveCursorToPoint(0, newLoc)
|
|
|
|
// By default, Cocoa suppresses mouse events briefly after a call to warp the
|
|
// cursor to a new location. This is good if you want to draw the user's
|
|
// attention to the fact that the mouse is now in a particular location, but
|
|
// it's bad in this case; we get called as part of the handling of edge
|
|
// scrolling, which means the mouse is typically still in motion, and we want
|
|
// the cursor to keep moving smoothly uninterrupted.
|
|
//
|
|
// This function's main action is to toggle whether the mouse cursor is
|
|
// associated with the mouse position, but setting it to true when it's
|
|
// already true has the side-effect of cancelling this motion suppression.
|
|
//
|
|
// However, we must NOT call this when relative mouse mode is active,
|
|
// as it would break the pointer lock established by enableNativeRelativeMouseMode.
|
|
if !RelativeMouseState.shared.enabled {
|
|
CGAssociateMouseAndMouseCursorPosition(1 /* true */)
|
|
}
|
|
|
|
result(true)
|
|
|
|
case "enableNativeRelativeMouseMode":
|
|
let success = self.enableNativeRelativeMouseMode(channel: channel)
|
|
result(success)
|
|
|
|
case "disableNativeRelativeMouseMode":
|
|
self.disableNativeRelativeMouseMode()
|
|
result(true)
|
|
|
|
default:
|
|
result(FlutterMethodNotImplemented)
|
|
}
|
|
})
|
|
}
|
|
}
|