diff --git a/VoidLink.xcodeproj/project.pbxproj b/VoidLink.xcodeproj/project.pbxproj index 3dad69f3..06af66bf 100644 --- a/VoidLink.xcodeproj/project.pbxproj +++ b/VoidLink.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ 18A765BD2ECD51FB004784AA /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; }; 18B317032EE67A3A009B62C2 /* InertialScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B317022EE67A39009B62C2 /* InertialScroller.swift */; }; 18D43D7F2F050240008C326F /* PressureCurve.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D43D7E2F050240008C326F /* PressureCurve.swift */; }; + 18D43D812F050240008C326F /* PencilTipOffsetCalibrationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D43D802F050240008C326F /* PencilTipOffsetCalibrationViewController.swift */; }; 18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E4BCE92E891895000A8657 /* MotionHandler.swift */; }; 18FCADAF2ECAC23200787A96 /* PencilHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FCADAE2ECAC23200787A96 /* PencilHandler.swift */; }; 2A263D4A2FF3B3F800154DEA /* PublicExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D492FF3B3F800154DEA /* PublicExtensions.swift */; }; @@ -209,6 +210,7 @@ 18B317022EE67A39009B62C2 /* InertialScroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InertialScroller.swift; sourceTree = ""; }; 18BF141D2E0D7A00009DEE4F /* VoidLink v1.0.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "VoidLink v1.0.xcdatamodel"; sourceTree = ""; }; 18D43D7E2F050240008C326F /* PressureCurve.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PressureCurve.swift; sourceTree = ""; }; + 18D43D802F050240008C326F /* PencilTipOffsetCalibrationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PencilTipOffsetCalibrationViewController.swift; sourceTree = ""; }; 18E4BCE92E891895000A8657 /* MotionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionHandler.swift; sourceTree = ""; }; 18FCADAE2ECAC23200787A96 /* PencilHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PencilHandler.swift; sourceTree = ""; }; 2A039E562F72892B00546768 /* VoidLinkPreview.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VoidLinkPreview.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -773,6 +775,7 @@ 988FCD3F293B091B003050E2 /* KeyboardInputField.h */, 988FCD40293B091B003050E2 /* KeyboardInputField.m */, 18D43D7E2F050240008C326F /* PressureCurve.swift */, + 18D43D802F050240008C326F /* PencilTipOffsetCalibrationViewController.swift */, 18FCADAE2ECAC23200787A96 /* PencilHandler.swift */, 18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */, 18B317022EE67A39009B62C2 /* InertialScroller.swift */, @@ -1202,6 +1205,7 @@ 187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */, FB4678FA1A55FFAD00377732 /* DiscoveryManager.m in Sources */, 18D43D7F2F050240008C326F /* PressureCurve.swift in Sources */, + 18D43D812F050240008C326F /* PencilTipOffsetCalibrationViewController.swift in Sources */, 180E6E372E93BFDB0067C73A /* AlertControllerUtil.swift in Sources */, 98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */, FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */, diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard index ee21cfb9..d3558aad 100644 --- a/VoidLink/Base.lproj/iPad.storyboard +++ b/VoidLink/Base.lproj/iPad.storyboard @@ -1144,6 +1144,26 @@ + + + + + + + + + + + + + + + @@ -2342,6 +2362,8 @@ + + diff --git a/VoidLink/Database/TemporarySettings.h b/VoidLink/Database/TemporarySettings.h index 4861842f..7fb00ff5 100644 --- a/VoidLink/Database/TemporarySettings.h +++ b/VoidLink/Database/TemporarySettings.h @@ -118,6 +118,8 @@ // Pencil settings: @property (nonatomic, retain) NSNumber * pencilTickMode; @property (nonatomic, retain) NSNumber * pencilTickIntervalUs; +@property (nonatomic, retain) NSNumber * pencilTipOffsetX; +@property (nonatomic, retain) NSNumber * pencilTipOffsetY; - (id) initFromSettings:(Settings*)settings; diff --git a/VoidLink/Database/TemporarySettings.m b/VoidLink/Database/TemporarySettings.m index 72323b3c..62db404f 100644 --- a/VoidLink/Database/TemporarySettings.m +++ b/VoidLink/Database/TemporarySettings.m @@ -175,6 +175,8 @@ // Pencil settings: self.pencilTickMode = settings.pencilTickMode; self.pencilTickIntervalUs = settings.pencilTickIntervalUs; + self.pencilTipOffsetX = settings.pencilTipOffsetX; + self.pencilTipOffsetY = settings.pencilTipOffsetY; #endif diff --git a/VoidLink/Input/CommandManager.swift b/VoidLink/Input/CommandManager.swift index 1c55245f..93162e1a 100644 --- a/VoidLink/Input/CommandManager.swift +++ b/VoidLink/Input/CommandManager.swift @@ -154,8 +154,8 @@ import UIKit @objc public static let stickWheels: [String] = ["LSWHEEL", "RSWHEEL"] @objc public static let velocityBasedTouchPads: [String] = ["LSVPAD", "RSVPAD", "MOUSEPAD", "TRACKBALL", "LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL"] @objc public static let inertialTouchPads: [String] = ["LSVPAD", "RSVPAD", "TRACKBALL", "LSPAD", "RSPAD"] - @objc public static let verticalTouchPads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"] - @objc public static let bidirectionalVerticalTouchPads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"] + @objc public static let verticalTouchPads: [String] = ["LTPAD", "RTPAD", "DISCRETEWHEEL", "DSWHEEL"] + @objc public static let singleDimensionActiveTouchpads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"] @objc public static let functionalButtonCmds: [String] = [ "SETTINGS", "TOOLBOX", diff --git a/VoidLink/Input/CustomOSC/OSCProfilesManager.swift b/VoidLink/Input/CustomOSC/OSCProfilesManager.swift index ab5b8b9e..491b4595 100644 --- a/VoidLink/Input/CustomOSC/OSCProfilesManager.swift +++ b/VoidLink/Input/CustomOSC/OSCProfilesManager.swift @@ -244,7 +244,8 @@ class OSCProfilesManager: NSObject { do { let fileData = try Data(contentsOf: URL(fileURLWithPath: filePath), options: .mappedIfSafe) - if let profilesEncoded = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSMutableData.self, NSMutableArray.self], from: fileData) as? NSMutableArray { + let profilePayloadData = try ProfileFileContainer.unpackedData(from: fileData) + if let profilesEncoded = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSMutableData.self, NSMutableArray.self], from: profilePayloadData) as? NSMutableArray { importEncodedProfiles(profilesEncoded) if let selectedIndex = selectedIndex { setProfileToSelected(selectedIndex) diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift index 7b0b117b..353c6663 100644 --- a/VoidLink/Input/OnScreenWidgetView.swift +++ b/VoidLink/Input/OnScreenWidgetView.swift @@ -619,9 +619,11 @@ import ObjectiveC.runtime self.hasSensitivityY = CommandManager.touchPadCmds.contains(self.touchPadString) && !CommandManager.stickWheels.contains(self.touchPadString) self.hasSlideThreshold = CommandManager.mousePads.contains(self.touchPadString) - if CommandManager.bidirectionalVerticalTouchPads.contains(self.touchPadString){ + if CommandManager.singleDimensionActiveTouchpads.contains(self.touchPadString){ self.sensitivityYMin = -4.0 self.sensitivityYMax = 4.0 + self.sensitivityXMin = -4.0 + self.sensitivityXMax = 4.0 } if CommandManager.mousePads.contains(self.touchPadString){ @@ -2661,7 +2663,15 @@ import ObjectiveC.runtime self.updateTouchLocation(touch: touch) case "MOUSEWHEEL","WHEEL": self.weightedDeltaY = Int(self.deltaY*7.5*self.sensitivityFactorY) - if firstTouchMoved {LiSendHighResScrollEvent(Int16(self.weightedDeltaY))} + self.weightedDeltaX = Int(self.deltaX*7.5*self.sensitivityFactorX) + if firstTouchMoved { + if abs(weightedDeltaY)>abs(weightedDeltaX) { + LiSendHighResScrollEvent(Int16(self.weightedDeltaY)) + } + else { + LiSendHighResHScrollEvent(-Int16(self.weightedDeltaX)) + } + } self.updateTouchLocation(touch: touch) case "DISCRETEWHEEL", "DSWHEEL": let currentLocation = touch.location(in: self) diff --git a/VoidLink/Input/PencilHandler.swift b/VoidLink/Input/PencilHandler.swift index ff9901be..d1b6ebe0 100644 --- a/VoidLink/Input/PencilHandler.swift +++ b/VoidLink/Input/PencilHandler.swift @@ -21,6 +21,7 @@ import UIKit private var manualTick: Bool private var pencilTickEnabled: Bool private var pressureCurveEnabled: Bool = false + @objc private(set) var pencilTipOffset: CGPoint = .zero private var manualHoverFlag: Bool = false @objc static var hoverSupported: Bool = false // @objc static private(set) var autoHoverTermination: Bool = false @@ -66,6 +67,8 @@ import UIKit tickInterval = TimeInterval(settings.pencilTickIntervalUs.floatValue/1000000) manualTick = settings.pencilTickMode.intValue == PencilTickMode.ManualTick.rawValue pencilTickEnabled = settings.pencilTickMode.intValue != PencilTickMode.PencilTickDisabled.rawValue + pencilTipOffset = CGPoint(x: CGFloat(settings.pencilTipOffsetX?.floatValue ?? 0), + y: CGFloat(settings.pencilTipOffsetY?.floatValue ?? 0)) // initialMoveEventIndexLimit = UIScreen.main.maximumFramesPerSecond > 60 ? 4 : 2 initialMoveEventIndexLimit = 0 super.init() @@ -110,6 +113,7 @@ import UIKit IAPManager.checkPurchaseInfo(.PencilProPack) { info in self.pressureCurveEnabled = self.pressureCurveEnabled && info.valid self.pencilTickEnabled = self.pencilTickEnabled && info.valid + self.pencilTipOffset = info.valid ? self.pencilTipOffset : .zero self.pencilProEnabled = info.valid PencilHandler.pencilPausesNativeTouch = selectedProfile.pencilPausesNativeTouch && info.valid if info.valid { @@ -243,6 +247,7 @@ import UIKit for touch in touchBatch { let point = touch.preciseLocation(in: streamView) + .applying(CGAffineTransform(translationX: pencilTipOffset.x, y: pencilTipOffset.y)) let azimuth = touch.azimuthAngle(in: streamView) let altitude = touch.altitudeAngle let location = self.adjustCoordinatesForVideoArea(point: point) @@ -345,6 +350,10 @@ import UIKit @objc public func switchPencilHover(){ manualHoverFlag = !manualHoverFlag } + + @objc public func updatePencilTipOffset(x: CGFloat, y: CGFloat) { + pencilTipOffset = CGPoint(x: x, y: y) + } @objc public func enablePencilHover(){ manualHoverFlag = true diff --git a/VoidLink/Input/PencilTipOffsetCalibrationViewController.swift b/VoidLink/Input/PencilTipOffsetCalibrationViewController.swift new file mode 100644 index 00000000..2f5f120f --- /dev/null +++ b/VoidLink/Input/PencilTipOffsetCalibrationViewController.swift @@ -0,0 +1,361 @@ +// +// PencilTipOffsetCalibrationViewController.swift +// VoidLink +// +// Created by True砖家 on 2026/7/30. +// Copyright © 2026 True砖家 on Bilibili. All rights reserved. +// + +import UIKit + +private enum PencilTipOffsetCalibrationLayout { + static let containerWidth: CGFloat = 430 + static let containerHeight: CGFloat = 430 + static let navBarHeight: CGFloat = 50 + static let canvasTop: CGFloat = 66 + static let canvasWidth: CGFloat = 320 + static let canvasHeight: CGFloat = 260 + static let sliderTopSpacing: CGFloat = 18 + static let horizontalInset: CGFloat = 30 +} + +private final class PencilTipOffsetCanvasView: UIView { + var offset: CGPoint = .zero { + didSet { setNeedsDisplay() } + } + + private var rawTouchPoint: CGPoint? { + didSet { setNeedsDisplay() } + } + + override init(frame: CGRect) { + super.init(frame: frame) + isMultipleTouchEnabled = false + backgroundColor = UIColor(white: 0.97, alpha: 1) + layer.borderColor = UIColor.black.withAlphaComponent(0.12).cgColor + layer.borderWidth = 0.5 + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { return } + + let center = CGPoint(x: bounds.midX, y: bounds.midY) + context.setLineWidth(1) + context.setStrokeColor(UIColor.black.withAlphaComponent(0.14).cgColor) + context.move(to: CGPoint(x: 0, y: center.y)) + context.addLine(to: CGPoint(x: bounds.maxX, y: center.y)) + context.move(to: CGPoint(x: center.x, y: 0)) + context.addLine(to: CGPoint(x: center.x, y: bounds.maxY)) + context.strokePath() + + context.setStrokeColor(UIColor.systemBlue.withAlphaComponent(0.45).cgColor) + context.setLineWidth(2) + context.move(to: center) + context.addLine(to: CGPoint(x: center.x + offset.x, y: center.y + offset.y)) + context.strokePath() + + if let rawTouchPoint { + drawLandingPreview(in: context, rawPoint: rawTouchPoint) + } + + context.setFillColor(UIColor.black.withAlphaComponent(0.22).cgColor) + context.fillEllipse(in: CGRect(x: center.x - 5, y: center.y - 5, width: 10, height: 10)) + + let tipCenter = CGPoint(x: center.x + offset.x, y: center.y + offset.y) + context.setFillColor(UIColor.systemBlue.cgColor) + context.fillEllipse(in: CGRect(x: tipCenter.x - 5, y: tipCenter.y - 5, width: 10, height: 10)) + } + + private func drawLandingPreview(in context: CGContext, rawPoint: CGPoint) { + let correctedPoint = CGPoint(x: rawPoint.x + offset.x, y: rawPoint.y + offset.y) + + context.setFillColor(UIColor.black.withAlphaComponent(0.28).cgColor) + context.fillEllipse(in: CGRect(x: rawPoint.x - 5, y: rawPoint.y - 5, width: 10, height: 10)) + + context.setFillColor(UIColor.systemBlue.cgColor) + context.fillEllipse(in: CGRect(x: correctedPoint.x - 5, y: correctedPoint.y - 5, width: 10, height: 10)) + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + updateTouchPoint(from: touches) + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + updateTouchPoint(from: touches) + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + updateTouchPoint(from: touches) + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + rawTouchPoint = nil + } + + private func updateTouchPoint(from touches: Set) { + guard let touch = touches.first(where: { $0.type == .pencil }) else { return } + rawTouchPoint = touch.preciseLocation(in: self) + } +} + +@objc class PencilTipOffsetCalibrationViewController: UIViewController, UIGestureRecognizerDelegate { + private let container = UIView() + private let canvasView = PencilTipOffsetCanvasView() + private let xSlider = UISlider() + private let ySlider = UISlider() + private let xLabel = UILabel() + private let yLabel = UILabel() + private var saveButton = UIBarButtonItem() + private var persistedOffset: CGPoint = .zero + private var currentOffset: CGPoint = .zero + + override func viewDidLoad() { + super.viewDidLoad() + + view.isOpaque = false + view.backgroundColor = UIColor.clear + if #available(iOS 13.0, *) { + overrideUserInterfaceStyle = .light + } + + loadPersistedOffset() + setupContainer() + setupNavigationBar() + setupCanvas() + setupSliders() + applyOffset(currentOffset) + + NotificationCenter.default.addObserver( + self, + selector: #selector(pencilProPurchaseAborted(_:)), + name: AddOnProduct.PencilProPack.purchaseAbortedNotification(), + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(persistOffset), + name: AddOnProduct.PencilProPack.purchaseSucceededNotification(), + object: nil + ) + + let tap = UITapGestureRecognizer(target: self, action: #selector(dismissSelf)) + tap.delegate = self + view.addGestureRecognizer(tap) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + NotificationCenter.default.removeObserver(self) + } + + private func loadPersistedOffset() { + let dataMan = DataManager() + let settings = dataMan.retrieveSettings() + persistedOffset = CGPoint( + x: CGFloat(settings?.pencilTipOffsetX?.floatValue ?? 0), + y: CGFloat(settings?.pencilTipOffsetY?.floatValue ?? 0) + ) + currentOffset = persistedOffset + } + + private func setupContainer() { + container.translatesAutoresizingMaskIntoConstraints = false + container.backgroundColor = .white + container.layer.cornerRadius = 12 + container.layer.masksToBounds = true + container.layer.borderWidth = 0.5 + container.layer.borderColor = UIColor.black.withAlphaComponent(0.15).cgColor + view.addSubview(container) + + NSLayoutConstraint.activate([ + container.centerXAnchor.constraint(equalTo: view.centerXAnchor), + container.centerYAnchor.constraint(equalTo: view.centerYAnchor), + container.widthAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.containerWidth), + container.heightAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.containerHeight) + ]) + } + + private func setupNavigationBar() { + let navBar = UINavigationBar() + navBar.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(navBar) + + if #available(iOS 13.0, *) { + let appearance = UINavigationBarAppearance() + appearance.shadowColor = nil + appearance.backgroundColor = .white + navBar.standardAppearance = appearance + navBar.scrollEdgeAppearance = appearance + } else { + navBar.setBackgroundImage(UIImage(), for: .default) + navBar.shadowImage = UIImage() + navBar.isTranslucent = true + navBar.backgroundColor = .clear + } + + saveButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Save"), style: .plain, target: self, action: #selector(saveTapped)) + let resetButton = UIBarButtonItem(title: LocalizationHelper.localizedString(forKey: "Reset"), style: .plain, target: self, action: #selector(resetTapped)) + let navItem = UINavigationItem(title: LocalizationHelper.localizedString(forKey: "Pencil Tip Offset")) + navItem.leftBarButtonItems = [resetButton] + navItem.rightBarButtonItems = [saveButton] + + if #available(iOS 26.0, *) { + resetButton.hidesSharedBackground = true + saveButton.hidesSharedBackground = true + resetButton.tintColor = .tintColor + saveButton.tintColor = .tintColor + } + + navBar.items = [navItem] + + NSLayoutConstraint.activate([ + navBar.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor), + navBar.leadingAnchor.constraint(equalTo: container.leadingAnchor), + navBar.trailingAnchor.constraint(equalTo: container.trailingAnchor), + navBar.heightAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.navBarHeight) + ]) + } + + private func setupCanvas() { + canvasView.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(canvasView) + + NSLayoutConstraint.activate([ + canvasView.topAnchor.constraint(equalTo: container.topAnchor, constant: PencilTipOffsetCalibrationLayout.canvasTop), + canvasView.centerXAnchor.constraint(equalTo: container.centerXAnchor), + canvasView.widthAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.canvasWidth), + canvasView.heightAnchor.constraint(equalToConstant: PencilTipOffsetCalibrationLayout.canvasHeight) + ]) + } + + private func setupSliders() { + let sliderStack = UIStackView() + sliderStack.axis = .vertical + sliderStack.spacing = 10 + sliderStack.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(sliderStack) + + configureSlider(xSlider, action: #selector(sliderMoved(_:))) + configureSlider(ySlider, action: #selector(sliderMoved(_:))) + configureLabel(xLabel) + configureLabel(yLabel) + + sliderStack.addArrangedSubview(makeSliderRow(label: xLabel, slider: xSlider)) + sliderStack.addArrangedSubview(makeSliderRow(label: yLabel, slider: ySlider)) + + NSLayoutConstraint.activate([ + sliderStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: PencilTipOffsetCalibrationLayout.horizontalInset), + sliderStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -PencilTipOffsetCalibrationLayout.horizontalInset), + sliderStack.topAnchor.constraint(equalTo: canvasView.bottomAnchor, constant: PencilTipOffsetCalibrationLayout.sliderTopSpacing) + ]) + + xSlider.value = Float(currentOffset.x) + ySlider.value = Float(currentOffset.y) + updateLabels() + } + + private func configureSlider(_ slider: UISlider, action: Selector) { + slider.minimumValue = -15 + slider.maximumValue = 15 + slider.addTarget(self, action: action, for: .valueChanged) + if PublicUtils.liquidGlassEnabled, #available(iOS 13.0, *) { + slider.maximumTrackTintColor = .tertiarySystemFill + } + } + + private func configureLabel(_ label: UILabel) { + label.font = UIFont.systemFont(ofSize: 14) + label.textColor = .black + label.widthAnchor.constraint(equalToConstant: 110).isActive = true + } + + private func makeSliderRow(label: UILabel, slider: UISlider) -> UIStackView { + let row = UIStackView(arrangedSubviews: [label, slider]) + row.axis = .horizontal + row.alignment = .center + row.spacing = 12 + return row + } + + @objc private func sliderMoved(_ sender: UISlider) { + currentOffset = CGPoint(x: CGFloat(xSlider.value), y: CGFloat(ySlider.value)) + applyOffset(currentOffset) + } + + private func applyOffset(_ offset: CGPoint) { + canvasView.offset = offset + updateLabels() + } + + private func updateLabels() { + xLabel.text = "\(LocalizationHelper.localizedString(forKey: "X Offset")): \(String(format: "%.1f", currentOffset.x))" + yLabel.text = "\(LocalizationHelper.localizedString(forKey: "Y Offset")): \(String(format: "%.1f", currentOffset.y))" + } + + @objc private func resetTapped() { + xSlider.value = 0 + ySlider.value = 0 + currentOffset = .zero + applyOffset(currentOffset) + } + + @objc private func persistOffset() { + let dataMan = DataManager() + guard let settings = dataMan.retrieveSettings() else { return } + settings.pencilTipOffsetX = NSNumber(value: Float(currentOffset.x)) + settings.pencilTipOffsetY = NSNumber(value: Float(currentOffset.y)) + dataMan.saveData() + persistedOffset = currentOffset + PencilHandler.shared?.updatePencilTipOffset(x: currentOffset.x, y: currentOffset.y) + + DispatchQueue.main.async { + AlertControllerUtil.autoCompletion = true + AlertControllerUtil.showAlert( + in: self, + title: "", + message: LocalizationHelper.localizedString(forKey: "Pencil tip offset saved"), + withCancel: false, + buttonTitle: "", + countdown: 1 + ) + } + } + + @objc private func saveTapped() { + saveButton.isEnabled = false + IAPManager.checkPurchaseInfo(.PencilProPack) { info in + self.saveButton.isEnabled = true + if info.valid { + self.persistOffset() + } else { + IAPManager.inAppPurchaseAction(viewController: self, product: .PencilProPack) + } + } + } + + @objc private func pencilProPurchaseAborted(_ notification: Notification) { + guard let interruption = notification.object as? PurchaseInterruption else { return } + if interruption == .lowOSVersion { + AlertControllerUtil.showAlert( + in: self, + title: "", + message: LocalizationHelper.localizedString(forKey:"PencilProPackLowOSVersionTip"), + withCancel: false, + buttonTitle: LocalizationHelper.localizedString(forKey: "OK"), + countdown: 0 + ) + } + } + + @objc private func dismissSelf() { + dismiss(animated: true) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + return touch.view == view + } +} diff --git a/VoidLink/Input/StreamView.m b/VoidLink/Input/StreamView.m index 116836bc..31e86578 100644 --- a/VoidLink/Input/StreamView.m +++ b/VoidLink/Input/StreamView.m @@ -1000,6 +1000,9 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; } CGPoint location = [self adjustCoordinatesForVideoArea:[gesture locationInView:self]]; + PencilHandler* handler = PencilHandler.shared; + location = CGPointApplyAffineTransform(location, CGAffineTransformMakeTranslation(handler.pencilTipOffset.x, handler.pencilTipOffset.y)); + CGSize videoSize = [self getVideoAreaSize]; float distance = 0.0f; diff --git a/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents b/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents index 26d5a515..818441f1 100644 --- a/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents +++ b/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents @@ -90,6 +90,8 @@ + + @@ -125,4 +127,4 @@ - \ No newline at end of file + diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index 16bf797a..75c0b24d 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -13303,6 +13303,64 @@ } } }, + "Pencil Tip Offset" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pencil Tip Offset" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "笔尖偏移" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "筆尖偏移" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "筆尖偏移" + } + } + } + }, + "Pencil tip offset saved" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pencil tip offset saved" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "笔尖偏移已保存" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "筆尖偏移已儲存" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "筆尖偏移已儲存" + } + } + } + }, "Pencil hover" : { "extractionState" : "manual", "localizations" : { @@ -19111,6 +19169,35 @@ } } }, + "X Offset" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "X Offset" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "X 偏移" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "X 偏移" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "X 偏移" + } + } + } + }, "Yaw factor: %.2f" : { "extractionState" : "manual", "localizations" : { @@ -19134,6 +19221,35 @@ } } }, + "Y Offset" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Y Offset" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Y 偏移" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Y 偏移" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "Y 偏移" + } + } + } + }, "Yes" : { "extractionState" : "manual", "localizations" : { diff --git a/VoidLink/PresetConfigs/widgetTemplates.bin b/VoidLink/PresetConfigs/widgetTemplates.bin index 84cc2e73..481d3d62 100644 Binary files a/VoidLink/PresetConfigs/widgetTemplates.bin and b/VoidLink/PresetConfigs/widgetTemplates.bin differ diff --git a/VoidLink/PresetConfigs/widgetTemplatesIPhone.bin b/VoidLink/PresetConfigs/widgetTemplatesIPhone.bin index 18a81831..a22b2f45 100644 Binary files a/VoidLink/PresetConfigs/widgetTemplatesIPhone.bin and b/VoidLink/PresetConfigs/widgetTemplatesIPhone.bin differ diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileSelectorViewController.swift b/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileSelectorViewController.swift index eaa74954..4f2946e3 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileSelectorViewController.swift +++ b/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileSelectorViewController.swift @@ -138,7 +138,7 @@ private enum ProfilePayloadCompression: UInt8 { } } -private enum ProfileFileContainer { +enum ProfileFileContainer { private static let magic = Array("VLPR".utf8) private static let version: UInt8 = 1 private static let headerSize = 14 diff --git a/VoidLink/ViewControllers/MainFrameViewController.m b/VoidLink/ViewControllers/MainFrameViewController.m index 57797468..1a18ef26 100644 --- a/VoidLink/ViewControllers/MainFrameViewController.m +++ b/VoidLink/ViewControllers/MainFrameViewController.m @@ -1890,6 +1890,8 @@ static NSMutableSet* hostList; // if([UIScreen mainScreen].maximumFramesPerSecond < 65) settings.touchMoveEventInterval = @(60); settings.onscreenControls = @(OnScreenControlsLevelCustom); settings.pencilTickIntervalUs = @(1750); + settings.pencilTipOffsetX = @(0); + settings.pencilTipOffsetY = @(0); settings.oscLayoutToolFingers = @(99); settings.keyboardToggleFingers = @(99); diff --git a/VoidLink/ViewControllers/SettingsViewController.h b/VoidLink/ViewControllers/SettingsViewController.h index f514b7f0..0eb7856d 100644 --- a/VoidLink/ViewControllers/SettingsViewController.h +++ b/VoidLink/ViewControllers/SettingsViewController.h @@ -291,6 +291,9 @@ @property (strong, nonatomic) IBOutlet UIStackView *pencilTickIntervalStack; @property (strong, nonatomic) IBOutlet UISlider *pencilTickIntervalSlider; +@property (weak, nonatomic) IBOutlet UIStackView *pencilTipOffsetStack; +@property (weak, nonatomic) IBOutlet UISwitch *pencilTipOffsetSwitch; + @property (weak, nonatomic) IBOutlet UIStackView *doubleTapShortcutStack; @property (weak, nonatomic) IBOutlet UISwitch *doubleTapShortcutSwitch; diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index 7cc6c7a1..c3dc2a18 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -1074,6 +1074,8 @@ BOOL isCustomResolution(int resolutionSelected) { self.pencilTickIntervalStack.hasDynamicLabel = YES; [self addSetting:self.pencilTickIntervalStack ofId:@"pencilTickIntervalStack" to:pencilSection]; + [self addSetting:self.pencilTipOffsetStack ofId:@"pencilTipOffsetStack" to:pencilSection]; + self.pressureCurveStack.isGameProfileSetting = YES; [self addSetting:self.pressureCurveStack ofId:@"pressureCurveStack" to:pencilSection]; @@ -4069,6 +4071,7 @@ BOOL isCustomResolution(int resolutionSelected) { [self.squeezeShortcutSwitch setOn:false]; [self.pencilPausesNativeTouchSwitch setOn:false]; [self.disablePencilSlideGestureSwitch setOn:false]; + [self.pencilTipOffsetSwitch setOn:false]; NSNumber *value = notification.userInfo[@"interruption"]; @@ -4109,6 +4112,15 @@ BOOL isCustomResolution(int resolutionSelected) { } } +- (void)pencilTipOffsetSwitchFlipped:(UISwitch* )sender{ + if(sender.isOn && !settingsViewJustLoaded){ + PencilTipOffsetCalibrationViewController* calibrationVC = [[PencilTipOffsetCalibrationViewController alloc] init]; + calibrationVC.modalPresentationStyle = UIModalPresentationOverFullScreen; + self.definesPresentationContext = true; + [self presentViewController:calibrationVC animated:YES completion:nil]; + } +} + - (void)doubleTapShortcutSwitchFlipped:(UISwitch* )sender{ if(sender.isOn && !settingsViewJustLoaded){ [IAPManager checkPurchaseInfo:AddOnProductPencilProPack completion:^(PurchaseInfo* info) { @@ -4203,6 +4215,9 @@ BOOL isCustomResolution(int resolutionSelected) { [self.pencilTickIntervalSlider addTarget:self action:@selector(pencilTickIntervalSliderMoved:) forControlEvents:UIControlEventValueChanged]; [self.pencilTickIntervalSlider sendActionsForControlEvents:UIControlEventValueChanged]; + [self.pencilTipOffsetSwitch setOn:(fabs(tempSettings.pencilTipOffsetX.floatValue) > 0.01 || fabs(tempSettings.pencilTipOffsetY.floatValue) > 0.01)]; + [self.pencilTipOffsetSwitch addTarget:self action:@selector(pencilTipOffsetSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; + [self.pressureCurveSwitch addTarget:self action:@selector(pressureCurveSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; [self.doubleTapShortcutSwitch addTarget:self action:@selector(doubleTapShortcutSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings index cbe32d49..85d20c4b 100644 --- a/VoidLink/mul.lproj/iPad.xcstrings +++ b/VoidLink/mul.lproj/iPad.xcstrings @@ -5564,6 +5564,36 @@ } } }, + "pTO-ff-lbl.text" : { + "comment" : "Class = \"UILabel\"; text = \"Pencil Tip Offset\"; ObjectID = \"pTO-ff-lbl\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Pencil Tip Offset" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "笔尖偏移" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "筆尖偏移" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "筆尖偏移" + } + } + } + }, "Pwa-YK-6Q2.text" : { "comment" : "Class = \"UILabel\"; text = \"Opacity\"; ObjectID = \"Pwa-YK-6Q2\";", "extractionState" : "extracted_with_value", @@ -7254,4 +7284,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +}