From 20aba200b37daebdc8bbabd81e02d8f41be5a8b5 Mon Sep 17 00:00:00 2001 From: True Zhuanjia Date: Tue, 28 Apr 2026 14:25:25 +0800 Subject: [PATCH] new touchpad widget: absolute mousepad --- VoidLink/Input/CommandManager.swift | 6 +- VoidLink/Input/OnScreenWidgetView.swift | 117 ++++++++++++------ VoidLink/Input/UITouchUtil.swift | 2 +- VoidLink/Localization/Localizable.xcstrings | 29 ++++- ...LayoutOnScreenControlsViewController.swift | 2 +- VoidLinkPreview/VirtualKeyboardView.swift | 39 +++--- VoidLinkPreview/WidgetPickerView.swift | 1 + 7 files changed, 133 insertions(+), 63 deletions(-) diff --git a/VoidLink/Input/CommandManager.swift b/VoidLink/Input/CommandManager.swift index 07d83fab..6789f650 100644 --- a/VoidLink/Input/CommandManager.swift +++ b/VoidLink/Input/CommandManager.swift @@ -118,9 +118,9 @@ import UIKit "OSCBACK" ] - @objc public static let touchPadCmds: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD", "LSWHEEL", "RSWHEEL", "LTPAD", "RTPAD", "DS4TOUCH", "MOUSEPAD", "ABSMOUSE", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL", "DPAD", "TRACKBALL", "WASDPAD", "ARROWPAD"] - @objc public static let mousePadWithButtonActions: [String] = ["MOUSEPAD", "ABSMOUSE"] - @objc public static let mousePads: [String] = ["MOUSEPAD", "ABSMOUSE", "TRACKBALL"] + @objc public static let touchPadCmds: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD", "LSWHEEL", "RSWHEEL", "LTPAD", "RTPAD", "DS4TOUCH", "MOUSEPAD", "ABSMOUSEPAD", "ABSMOUSE", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL", "DPAD", "TRACKBALL", "WASDPAD", "ARROWPAD"] + @objc public static let mousePadWithButtonActions: [String] = ["MOUSEPAD", "ABSMOUSE", "ABSMOUSEPAD"] + @objc public static let mousePads: [String] = ["MOUSEPAD", "ABSMOUSE", "TRACKBALL", "ABSMOUSEPAD"] @objc public static let directionPads: [String] = ["DPAD", "WASDPAD", "ARROWPAD"] @objc public static let stickTouchPads: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD"] @objc public static let nonVectorStickPads: [String] = ["LSPAD", "RSPAD"] diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift index cda9f4f9..2289cc3e 100644 --- a/VoidLink/Input/OnScreenWidgetView.swift +++ b/VoidLink/Input/OnScreenWidgetView.swift @@ -129,7 +129,8 @@ import ObjectiveC.runtime @objc public var initialCenter: CGPoint = .zero // location from persisted data @objc public var layoutChanges: [CGPoint] = [] @objc public var mouseButtonAction: MouseButtonAction = .hovering; - + @objc public var mouseButtonActionDelay: TimeInterval = 0.005; + //autoTapTimer @objc public var autoTapInterval: Int = 49; private var autoTapTimer: SafeTimer? @@ -279,7 +280,7 @@ import ObjectiveC.runtime // for mousePad private var touchBegan: Bool = false private var directionPadTouchBegan: Bool = false - private var firstTouchMoved: Bool = false + @objc private(set) var firstTouchMoved: Bool = false private var mousePointerMoved: Bool private var twoTouchesDetected: Bool private var allSpawnedTouchesCount: Int = 0 @@ -329,6 +330,7 @@ import ObjectiveC.runtime @objc public var bulkMoveEnabled: Bool = false @objc public var sequenceSet: Set = Set() @objc public var parentSequence: Int16 = -1 + @objc public var standardFoldingInterval: TimeInterval = 0.05 static weak var capturer: OnScreenWidgetView? @objc init(cmdString: String, buttonLabel: String, shape:String, profile:OSCProfile) { @@ -535,6 +537,10 @@ import ObjectiveC.runtime self.hasTrackPoint = true self.hasNonEditableLabel = (self.cmdString == "DISABLETOUCH" || self.cmdString == "GAMEPADOVERLAY") + + self.mouseButtonActionDelay = self.cmdString.contains("ABSMOUSEPAD") ? 0.005 : 0 + + self.standardFoldingInterval = widgetType == .touchPad ? 0.05 : 0.15; } // ====================================================================================================== @@ -2076,7 +2082,7 @@ import ObjectiveC.runtime } if self.widgetType == WidgetTypeEnum.touchPad && CommandManager.mousePadWithButtonActions.contains(self.touchPadString) && allSpawnedTouchesCount == 1 && !twoTouchesDetected { - self.handleMousePadButtonActionDown() + self.handleMousePadButtonActionDown(touch: touch) } } @@ -2433,6 +2439,10 @@ import ObjectiveC.runtime } self.updateTouchLocation(touch: touch) break + case "ABSMOUSEPAD": + let touchLocation = touch.location(in: self.superview) + LiSendMousePositionEvent(Int16(touchLocation.x), Int16(touchLocation.y), Int16(self.superViewWidth), Int16(self.superViewHeight)) + break case "LSWHEEL", "RSWHEEL": self.handleStickWheelMove(touch: touch) case "LSPAD": @@ -2507,47 +2517,72 @@ import ObjectiveC.runtime } } - private func handleMousePadButtonActionUp(){ - switch self.mouseButtonAction{ - case .leftButtonDown: - LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_LEFT) - case .middleButtonDown: - LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_MIDDLE) - case .rightButtonDown: - LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_RIGHT) - case .hovering: - if !self.mousePointerMoved && !self.quickDoubleTapDetected {self.sendLongMouseLeftButtonClickEvent()} // deal with single tap(click) - if self.quickDoubleTapDetected { //deal with quick double tap - LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_LEFT) //must release the button anyway, because the button is likely being held down since the long click turned into a dragging event. - if !self.mousePointerMoved {self.sendShortMouseLeftButtonClickEvent()} - self.quickDoubleTapDetected = false + private func handleMousePadButtonActionUp(touch:UITouch?=nil){ + if let touch = touch { + let touchLocation = touch.location(in: self.superview) + switch touchPadString { + case "ABSMOUSEPAD": + LiSendMousePositionEvent(Int16(touchLocation.x), Int16(touchLocation.y), Int16(self.superViewWidth), Int16(self.superViewHeight)) + default: + break + } + } + + DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + self.mouseButtonActionDelay) { + switch self.mouseButtonAction{ + case .leftButtonDown: + LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_LEFT) + case .middleButtonDown: + LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_MIDDLE) + case .rightButtonDown: + LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_RIGHT) + case .hovering: + if !self.mousePointerMoved && !self.quickDoubleTapDetected {self.sendLongMouseLeftButtonClickEvent()} // deal with single tap(click) + if self.quickDoubleTapDetected { //deal with quick double tap + LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_LEFT) //must release the button anyway, because the button is likely being held down since the long click turned into a dragging event. + if !self.mousePointerMoved {self.sendShortMouseLeftButtonClickEvent()} + self.quickDoubleTapDetected = false + } + self.mousePointerMoved = false // reset this flag + case .noClick: + // quickDoubleTapDetected = false + break + default: + break } - self.mousePointerMoved = false // reset this flag - case .noClick: - // quickDoubleTapDetected = false - break - default: - break } } - private func handleMousePadButtonActionDown(){ - switch mouseButtonAction{ - case .leftButtonDown: - LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), BUTTON_LEFT) - case .middleButtonDown: - LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), BUTTON_MIDDLE) - case .rightButtonDown: - LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), BUTTON_RIGHT) - case .noClick: - if quickDoubleTapDetected && !self.comboButtonStrings.isEmpty { - self.showl3r3Indicator() - self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings) + + private func handleMousePadButtonActionDown(touch:UITouch?=nil){ + DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + self.mouseButtonActionDelay) { + switch self.mouseButtonAction{ + case .leftButtonDown: + LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), BUTTON_LEFT) + case .middleButtonDown: + LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), BUTTON_MIDDLE) + case .rightButtonDown: + LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), BUTTON_RIGHT) + case .noClick: + if self.quickDoubleTapDetected && !self.comboButtonStrings.isEmpty { + self.showl3r3Indicator() + self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings) + } + case .hovering: + break + default: + break } - case .hovering: - break - default: - break + } + + if let touch = touch { + let touchLocation = touch.location(in: self.superview) + switch touchPadString { + case "ABSMOUSEPAD": + LiSendMousePositionEvent(Int16(touchLocation.x), Int16(touchLocation.y), Int16(self.superViewWidth), Int16(self.superViewHeight)) + default: + break + } } } @@ -3460,7 +3495,7 @@ import ObjectiveC.runtime || abs(widget.backgroundAlpha) < 0.1){ widget.highlightBorder(highlighted: true, color: UIColor.systemBlue.cgColor) } - let duration = OnScreenWidgetView.enableFolderAnimation ? (folder.buttonMode == .slideAndHold ? 0.05 : 0.15) : 0 + let duration = OnScreenWidgetView.enableFolderAnimation ? (folder.buttonMode == .slideAndHold ? 0.05 : widget.standardFoldingInterval) : 0 UIView.animate(withDuration: duration, animations: { widget.center = folder.center },completion: { finished in @@ -3487,7 +3522,7 @@ import ObjectiveC.runtime || abs(widget.backgroundAlpha) < 0.1){ widget.highlightBorder(highlighted: true, color: UIColor.systemBlue.cgColor) } - UIView.animate(withDuration: OnScreenWidgetView.enableFolderAnimation ? (folder.buttonMode == .slideAndHold ? 0.05 : 0.15) : 0, animations: { + UIView.animate(withDuration: OnScreenWidgetView.enableFolderAnimation ? (folder.buttonMode == .slideAndHold ? 0.05 : widget.standardFoldingInterval) : 0, animations: { widget.center = widget.storedCenter },completion: { finished in widget.capturedTouches.removeAllObjects() diff --git a/VoidLink/Input/UITouchUtil.swift b/VoidLink/Input/UITouchUtil.swift index 861f423e..086a334c 100644 --- a/VoidLink/Input/UITouchUtil.swift +++ b/VoidLink/Input/UITouchUtil.swift @@ -11,7 +11,7 @@ import UIKit @objc class UITouchUtil: NSObject { @objc public static func touches(in view: UIView, from event: UIEvent?) -> Set { guard (event?.allTouches) != nil else { return [] } - return event?.allTouches?.filter({ $0.view == view && $0.type == .direct}) ?? [] + return event?.allTouches?.filter({ $0.view == view && ($0.type == .direct || $0.type == .pencil)}) ?? [] } @objc public static func distance(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGFloat { diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index b064f46b..4a6c3f45 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -1526,6 +1526,29 @@ } } }, + "Absolute MousePad" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠标触控板(绝对坐标)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "滑鼠觸控板(絕對座標)" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "滑鼠觸控板(絕對座標)" + } + } + } + }, "Absolute touch drag" : { "extractionState" : "manual", "localizations" : { @@ -1600,19 +1623,19 @@ "localizations" : { "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "添加主机" } }, "zh-Hant" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "新增主機" } }, "zh-HK" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "新增主機" } } diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift index ae5bdb71..fd6084df 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift @@ -2305,7 +2305,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg view.sendSubviewToBack(selectedWidgetView) } } - if !isToolbarHidden, let selectedWidgetView, layerIsOverlappingWithTrashcanButton(selectedWidgetView.layer) { + if !isToolbarHidden, let selectedWidgetView, layerIsOverlappingWithTrashcanButton(selectedWidgetView.layer), selectedWidgetView.firstTouchMoved { OnScreenWidgetView.setFree(widget: selectedWidgetView) OnScreenWidgetView.removeWidgetFromMappings(key: selectedWidgetView.sequence) selectedWidgetView.removeFromSuperview() diff --git a/VoidLinkPreview/VirtualKeyboardView.swift b/VoidLinkPreview/VirtualKeyboardView.swift index 0d57e56e..fe1e4f80 100644 --- a/VoidLinkPreview/VirtualKeyboardView.swift +++ b/VoidLinkPreview/VirtualKeyboardView.swift @@ -638,7 +638,7 @@ struct VirtualKeyboardView: View { } private func isPadLikeControl(_ key: Key) -> Bool { - let padCommands: Set = ["WHEEL", "MOUSEPAD", "TRACKBALL"] + let padCommands: Set = ["WHEEL", "MOUSEPAD", "TRACKBALL", "ABSMOUSEPAD"] if let cmd = key.cmdString(for: keyboardType) { return padCommands.contains(cmd) } @@ -1080,18 +1080,28 @@ struct VirtualKeyboardView: View { ), height: keyHeight * 2 + spacing ) - - keyboardKeyView( - Key( - label: SwiftLocalizationHelper.localizedString(forKey: "Trackball"), - width: unit * 2 + spacing, - winCmdString: "TRACKBALL", - macCmdString: "TRACKBALL", - winKeyCode: 0xFF, - macKeyCode: 0xFF - ), - height: keyHeight * 2 + spacing - ) + VStack(spacing: spacing) { + keyboardKeyView( + Key( + label: SwiftLocalizationHelper.localizedString(forKey: "Absolute MousePad"), + width: unit * 2 + spacing, + winCmdString: "ABSMOUSEPAD", + macCmdString: "ABSMOUSEPAD", + winKeyCode: 0xFF, + macKeyCode: 0xFF + ), + ) + keyboardKeyView( + Key( + label: SwiftLocalizationHelper.localizedString(forKey: "Trackball"), + width: unit * 2 + spacing, + winCmdString: "TRACKBALL", + macCmdString: "TRACKBALL", + winKeyCode: 0xFF, + macKeyCode: 0xFF + ), + ) + } } } } @@ -1274,12 +1284,13 @@ struct VirtualKeyboardView: View { Key(label: SwiftLocalizationHelper.localizedString(forKey: "Wheel\nDown"), width: unit, winCmdString: "WHEELUP", macCmdString: "WHEELUP", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: SwiftLocalizationHelper.localizedString(forKey: "WheelPad"), width: unit * 2 + spacing, winCmdString: "WHEEL", macCmdString: "WHEEL", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: SwiftLocalizationHelper.localizedString(forKey: "MousePad"), width: unit * 2 + spacing, winCmdString: "MOUSEPAD", macCmdString: "MOUSEPAD", winKeyCode: 0xFF, macKeyCode: 0xFF), + Key(label: SwiftLocalizationHelper.localizedString(forKey: "Absolute MousePad"), width: unit * 2 + spacing, winCmdString: "ABSMOUSEPAD", macCmdString: "ABSMOUSEPAD", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: SwiftLocalizationHelper.localizedString(forKey: "Trackball"), width: unit * 2 + spacing, winCmdString: "TRACKBALL", macCmdString: "TRACKBALL", winKeyCode: 0xFF, macKeyCode: 0xFF) ] if mode == .shortcutPicker { keys.removeAll { key in let command = key.cmdString(for: keyboardType) ?? "" - return command == "WHEEL" || command == "MOUSEPAD" || command == "TRACKBALL" + return command == "WHEEL" || command == "MOUSEPAD" || command == "TRACKBALL" || command == "ABSMOUSEPAD" } } return keys diff --git a/VoidLinkPreview/WidgetPickerView.swift b/VoidLinkPreview/WidgetPickerView.swift index 2d337804..9ee99fed 100644 --- a/VoidLinkPreview/WidgetPickerView.swift +++ b/VoidLinkPreview/WidgetPickerView.swift @@ -1257,6 +1257,7 @@ struct WidgetPickerView: View { "ARROWPAD", "WHEEL", "MOUSEPAD", + "ABSMOUSEPAD", "TRACKBALL" ] }