Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Mos/ScrollCore/ScrollCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,18 @@ class ScrollCore {
if scrollEvent.isTrackpad() {
return Unmanaged.passUnretained(event)
}
// 当事件来自远程桌面,且其发送的事件 isContinuous=1.0,此时跳过本地平滑
if ScrollUtils.shared.isRemoteSmoothedEvent(event) {
return Unmanaged.passUnretained(event)
}
// 当鼠标输入, 根据需要执行翻转方向/平滑滚动
// 获取事件目标
let targetRunningApplication = ScrollUtils.shared.getRunningApplication(from: event)
// ToDesk 等远程控制链路无法稳定处理 Mos 合成的连续滚轮事件,只禁用平滑,保留翻转等原始事件处理。
let disableSmoothForRemoteControl = ScrollUtils.shared.shouldDisableSmoothForRemoteControl(
event,
targetRunningApplication: targetRunningApplication
)
let isRemoteSmoothedEvent = ScrollUtils.shared.isRemoteSmoothedEvent(event)
if isRemoteSmoothedEvent && !disableSmoothForRemoteControl {
return Unmanaged.passUnretained(event)
}
// 获取列表中应用程序的列外设置信息
ScrollCore.shared.application = ScrollUtils.shared.getTargetApplication(from: targetRunningApplication)
// 平滑/翻转
Expand Down Expand Up @@ -117,6 +122,11 @@ class ScrollCore {
enableReverseVertical = allowReverse && Options.shared.scroll.reverseVertical
enableReverseHorizontal = allowReverse && Options.shared.scroll.reverseHorizontal
}
if disableSmoothForRemoteControl || isRemoteSmoothedEvent {
enableSmooth = false
enableSmoothVertical = false
enableSmoothHorizontal = false
}
// Launchpad 激活则强制屏蔽平滑
if ScrollUtils.shared.getLaunchpadActivity(withRunningApplication: targetRunningApplication) {
enableSmooth = false
Expand Down
113 changes: 91 additions & 22 deletions Mos/ScrollCore/ScrollUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,33 +121,21 @@ class ScrollUtils {
// 远程桌面事件检测缓存
private var lastSourcePID: pid_t = 0
private var lastSourceIsRemoteControl: Bool = false
private var lastSourceNeedsRawScrollPassthrough: Bool = false

/// 检测事件来源是否为远程桌面应用
func isFromRemoteApplication(_ event: CGEvent) -> Bool {
let sourcePID = pid_t(event.getIntegerValueField(.eventSourceUnixProcessID))
if sourcePID == 0 { return false }

if sourcePID != lastSourcePID {
lastSourcePID = sourcePID
lastSourceIsRemoteControl = false
refreshRemoteSourceCacheIfNeeded(event)
return lastSourceIsRemoteControl
}

if let app = NSRunningApplication(processIdentifier: sourcePID) {
// 检查可执行文件路径(系统守护进程)
if let path = app.executableURL?.path {
for keyword in REMOTE_CONTROL_APPLICATION.executableKeywords {
if path.contains(keyword) {
lastSourceIsRemoteControl = true
break
}
}
}
// 检查 Bundle Identifier(第三方应用)
if !lastSourceIsRemoteControl, let bundleId = app.bundleIdentifier {
lastSourceIsRemoteControl = REMOTE_CONTROL_APPLICATION.bundleIdentifiers.contains(bundleId)
}
}
/// ToDesk 等远程控制应用对 Mos 合成的连续滚轮事件兼容性差,需禁用平滑但保留方向翻转等原始事件处理。
func shouldDisableSmoothForRemoteControl(_ event: CGEvent, targetRunningApplication: NSRunningApplication?) -> Bool {
if needsRawScrollPassthrough(from: targetRunningApplication) {
return true
}
return lastSourceIsRemoteControl
refreshRemoteSourceCacheIfNeeded(event)
return lastSourceNeedsRawScrollPassthrough
}

/// 检测事件是否来自已被平滑的远程源
Expand All @@ -159,6 +147,87 @@ class ScrollUtils {
return isContinuous == 1.0 // 1.0 表示主控端已平滑
}

func isKnownRemoteControlApplication(executablePath: String?, bundleIdentifier: String?) -> Bool {
if containsAnyKeyword(in: executablePath, keywords: REMOTE_CONTROL_APPLICATION.executableKeywords) {
return true
}
if let bundleIdentifier = bundleIdentifier,
REMOTE_CONTROL_APPLICATION.bundleIdentifiers.contains(bundleIdentifier) {
return true
}
return containsAnyKeyword(
in: bundleIdentifier,
keywords: REMOTE_CONTROL_APPLICATION.bundleIdentifierKeywords
)
}

func needsRawScrollPassthrough(executablePath: String?, bundleIdentifier: String?) -> Bool {
return containsAnyKeyword(
in: executablePath,
keywords: REMOTE_CONTROL_APPLICATION.rawScrollPassthroughExecutableKeywords
) || containsAnyKeyword(
in: bundleIdentifier,
keywords: REMOTE_CONTROL_APPLICATION.rawScrollPassthroughBundleIdentifierKeywords
)
}

private func needsRawScrollPassthrough(from runningApplication: NSRunningApplication?) -> Bool {
guard let runningApplication = runningApplication else { return false }
return needsRawScrollPassthrough(
executablePath: runningApplication.executableURL?.path,
bundleIdentifier: runningApplication.bundleIdentifier
) || needsRawScrollPassthrough(
executablePath: runningApplication.bundleURL?.path,
bundleIdentifier: runningApplication.bundleIdentifier
)
}

private func refreshRemoteSourceCacheIfNeeded(_ event: CGEvent) {
let sourcePID = pid_t(event.getIntegerValueField(.eventSourceUnixProcessID))
if sourcePID == 0 {
lastSourcePID = 0
lastSourceIsRemoteControl = false
lastSourceNeedsRawScrollPassthrough = false
return
}

if sourcePID != lastSourcePID {
lastSourcePID = sourcePID
lastSourceIsRemoteControl = false
lastSourceNeedsRawScrollPassthrough = false

guard let app = NSRunningApplication(processIdentifier: sourcePID) else { return }
let executablePath = app.executableURL?.path
let bundlePath = app.bundleURL?.path
let bundleIdentifier = app.bundleIdentifier

lastSourceIsRemoteControl = isKnownRemoteControlApplication(
executablePath: executablePath,
bundleIdentifier: bundleIdentifier
) || isKnownRemoteControlApplication(
executablePath: bundlePath,
bundleIdentifier: bundleIdentifier
)
lastSourceNeedsRawScrollPassthrough = needsRawScrollPassthrough(
executablePath: executablePath,
bundleIdentifier: bundleIdentifier
) || needsRawScrollPassthrough(
executablePath: bundlePath,
bundleIdentifier: bundleIdentifier
)
}
}

private func containsAnyKeyword(in value: String?, keywords: [String]) -> Bool {
guard let lowercasedValue = value?.lowercased() else { return false }
for keyword in keywords {
if lowercasedValue.contains(keyword.lowercased()) {
return true
}
}
return false
}

// MARK: - 滚动参数: 热键
// 返回 ScrollHotkey? 供 ScrollCore 使用
func optionsDashKey(application: Application?) -> ScrollHotkey? {
Expand Down
14 changes: 14 additions & 0 deletions Mos/Utils/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ struct REMOTE_CONTROL_APPLICATION {
"screensharingd", // macOS 屏幕共享守护进程
"ScreensharingAgent", // macOS 屏幕共享用户会话代理
"ARDAgent", // Apple Remote Desktop
"ToDesk", // ToDesk 远程控制相关进程
"todesk", // ToDesk helper/daemon 可能使用小写路径
]
// Bundle Identifier(用于第三方应用)
static let bundleIdentifiers = [
Expand All @@ -83,6 +85,18 @@ struct REMOTE_CONTROL_APPLICATION {
"com.tigervnc.vncviewer",
"com.netease.uuremote", // UU 远程桌面
]
// Bundle Identifier 关键字(用于 ToDesk 这类版本间可能变化的 ID)
static let bundleIdentifierKeywords = [
"todesk",
]
// 这些远程控制应用无法稳定处理 Mos 合成的连续滚轮事件,需直接使用原始滚轮。
static let rawScrollPassthroughExecutableKeywords = [
"ToDesk",
"todesk",
]
static let rawScrollPassthroughBundleIdentifierKeywords = [
"todesk",
]
}

enum ScrollDurationLimits {
Expand Down
34 changes: 34 additions & 0 deletions MosTests/ScrollEventTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,40 @@ final class ScrollEventTests: XCTestCase {
return event
}

// MARK: - 远程控制应用识别

func testRemoteControlApplicationDetectsToDeskExecutableKeyword() {
XCTAssertTrue(ScrollUtils.shared.isKnownRemoteControlApplication(
executablePath: "/Applications/ToDesk.app/Contents/MacOS/ToDesk",
bundleIdentifier: nil
))
}

func testRemoteControlApplicationDetectsToDeskBundleIdentifierKeyword() {
XCTAssertTrue(ScrollUtils.shared.isKnownRemoteControlApplication(
executablePath: nil,
bundleIdentifier: "com.youqu.todesk"
))
}

func testRemoteControlApplicationRequiresRawPassthroughForToDesk() {
XCTAssertTrue(ScrollUtils.shared.needsRawScrollPassthrough(
executablePath: "/Library/Application Support/ToDesk/ToDesk_Service",
bundleIdentifier: nil
))
}

func testRemoteControlApplicationDoesNotRequireRawPassthroughForOtherRemoteApps() {
XCTAssertTrue(ScrollUtils.shared.isKnownRemoteControlApplication(
executablePath: nil,
bundleIdentifier: "com.teamviewer.TeamViewer"
))
XCTAssertFalse(ScrollUtils.shared.needsRawScrollPassthrough(
executablePath: nil,
bundleIdentifier: "com.teamviewer.TeamViewer"
))
}

// MARK: - initEvent: 优先级 (scrollPt > scrollFixPt > scrollFix)

func testInitEvent_prefersPtOverFixPt() throws {
Expand Down