- genshin touch control enhancement preview for settings menu

- controller navigation & first connection tips
- enhancedTouchPreview bug fix
This commit is contained in:
True Zhuanjia
2026-07-30 14:14:33 +08:00
parent b0c16922a2
commit e0aff4b532
10 changed files with 656 additions and 8 deletions
+4
View File
@@ -47,6 +47,7 @@
2AC1E3832F507D80004F76C0 /* GenericUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1E3822F507D80004F76C0 /* GenericUtils.swift */; };
2AC1EAFB2F92260F004EDA80 /* UIStackExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */; };
2AC1EAFC2F922889004EDA80 /* UIStackExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */; };
2B7A10022FFB000100000001 /* SettingsTouchVelocityPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7A10012FFB000100000001 /* SettingsTouchVelocityPreview.swift */; };
3A4A3A112F0C000100000001 /* ProfileSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4A3A102F0C000100000001 /* ProfileSelectorViewController.swift */; };
3A4A3A132F0C000100000003 /* LayoutOnScreenControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4A3A122F0C000100000002 /* LayoutOnScreenControlsViewController.swift */; };
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; };
@@ -218,6 +219,7 @@
2A7114002F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GamepadNavigationIllustrationHud.swift; sourceTree = "<group>"; };
2AC1E3822F507D80004F76C0 /* GenericUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericUtils.swift; sourceTree = "<group>"; };
2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackExtension.swift; sourceTree = "<group>"; };
2B7A10012FFB000100000001 /* SettingsTouchVelocityPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTouchVelocityPreview.swift; sourceTree = "<group>"; };
3A4A3A102F0C000100000001 /* ProfileSelectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSelectorViewController.swift; sourceTree = "<group>"; };
3A4A3A122F0C000100000002 /* LayoutOnScreenControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutOnScreenControlsViewController.swift; sourceTree = "<group>"; };
693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
@@ -864,6 +866,7 @@
FB89462719F646E200339C8A /* StreamFrameViewController.m */,
FBD3494E19FF2174002D2A60 /* SettingsViewController.h */,
FBD3494F19FF2174002D2A60 /* SettingsViewController.m */,
2B7A10012FFB000100000001 /* SettingsTouchVelocityPreview.swift */,
2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */,
FB4A23B61A9D3637004D2EF2 /* LoadingFrameViewController.h */,
FB4A23B71A9D3637004D2EF2 /* LoadingFrameViewController.m */,
@@ -1188,6 +1191,7 @@
97E20DEA2E216532000D5DCA /* SceneDelegate.m in Sources */,
1817B61E2E6FD04D000D938D /* SafeTimer.swift in Sources */,
FBD3495019FF2174002D2A60 /* SettingsViewController.m in Sources */,
2B7A10022FFB000100000001 /* SettingsTouchVelocityPreview.swift in Sources */,
843CC7F02C1E757D00BBC475 /* NativeTouchPointer.m in Sources */,
2A7113FD2F5A6C65001D852D /* ThemeManager.swift in Sources */,
FB89462C19F646E200339C8A /* HttpManager.m in Sources */,
+3 -1
View File
@@ -338,6 +338,7 @@ final class ControllerNavigator: NSObject {
}
static func updateUINavigationDelegateState() {
ControllerUtil.disableSysGestures(ControllerUtil.primaryGCController)
if uiNavigationDelegate is StreamFrameViewController {
if radialMenuState == .mouseModeEnabled {
startControllerMouse()
@@ -2273,7 +2274,8 @@ extension ControllerCollectionNavigationDelegate {
extension MainFrameViewController {
func forceLaunchApp(_ app: TemporaryApp?){
if let currentRunningApp = self.findRunningApp(app?.host) {
guard let app = app else { return }
if let currentRunningApp = self.findRunningApp(app.host) {
if currentRunningApp !== app {
self.quitRunningAppAndStart(app)
}
+12
View File
@@ -466,6 +466,18 @@ import Foundation
guard GCController.controllers().count == 1 else { return }
if let mainFrameVC = delegate as? MainFrameViewController {
let vc = mainFrameVC.isStreaming() ? StreamFrameViewController.sharedInstance() : mainFrameVC
GenericUtils.handleFirstGamepadConnection(in: vc) {
setGCControllerToPrimary(controller)
return
}
}
setGCControllerToPrimary(controller)
}
private static func setGCControllerToPrimary(_ controller: GCController) {
primaryGCController = controller
_ = buildMapping(for: controller, swapABXY: false)
disableSysGestures(controller)
+198 -1
View File
@@ -536,6 +536,35 @@
}
}
},
"%.0f%% velocity" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%.0f%% velocity"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%.0f%% 移速缩放"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "%.0f%% 移速縮放"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "%.0f%% 移速縮放"
}
}
}
},
"%@ is currently running" : {
"extractionState" : "manual",
"localizations" : {
@@ -6269,6 +6298,35 @@
}
}
},
"controllerNavigationTip" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "VoidLink now supports global controller navigation! Go to [Settings Menu] → [Controller] section → turn [Controller Navigation] off and on again to customize radial menu buttons and controller mouse mode."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "VoidLink已支持全局手柄导航!前往 [设置菜单] → [手柄] 分区 → 关闭再打开 [手柄导航],即可自定义环形菜单按键与手柄鼠标模式。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "VoidLink 已支援全域手把導航!前往 [設定選單] → [手把] 分區 → 關閉再開啟 [手把導航],即可自訂環形選單按鍵與手把滑鼠模式。"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "VoidLink 已支援全域手把導航!前往 [設定選單] → [手把] 分區 → 關閉再開啟 [手把導航],即可自訂環形選單按鍵與手把滑鼠模式。"
}
}
}
},
"Create" : {
"extractionState" : "manual",
"localizations" : {
@@ -6678,6 +6736,35 @@
}
}
},
"descendants" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "descendants"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "衍生版"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "衍生版"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "衍生版"
}
}
}
},
"Detailed Tutorial" : {
"extractionState" : "manual",
"localizations" : {
@@ -7750,6 +7837,35 @@
}
}
},
"Enhanced Touch Control for Genshin Game Streaming" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Enhanced Touch Control for Genshin Game Streaming"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "原神串流增强触控"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "原神串流增強觸控"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "原神串流增強觸控"
}
}
}
},
"Enter a new command and alias" : {
"extractionState" : "manual",
"localizations" : {
@@ -12922,6 +13038,35 @@
}
}
},
"Originally designed & coded by True Zhuanjia with pride" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Originally designed & coded by True Zhuanjia with pride"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "由 True砖家@Bilibili 自豪地原创设计并编码"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "由 True砖家@Bilibili 自豪地原創設計並編碼"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "由 True砖家@Bilibili 自豪地原創設計並編碼"
}
}
}
},
"Others" : {
"extractionState" : "manual",
"localizations" : {
@@ -13083,6 +13228,35 @@
}
}
},
"passthrough" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "passthrough"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "坐标透传"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "座標透傳"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "座標透傳"
}
}
}
},
"Pen Pressure Curve" : {
"extractionState" : "manual",
"localizations" : {
@@ -17252,6 +17426,29 @@
}
}
},
"Tap to learn more" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "点击了解更多"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "點擊了解更多"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "點擊了解更多"
}
}
}
},
"Tap to toggle" : {
"extractionState" : "manual",
"localizations" : {
@@ -19046,4 +19243,4 @@
}
},
"version" : "1.1"
}
}
+28
View File
@@ -442,6 +442,34 @@ import UIKit
return false
}
@objc public static func isFirstConnectingGamepad() -> Bool {
let key = "isFirstConnectingGamepad20260730"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc public static func handleFirstGamepadConnection(in vc: UIViewController?, handler: @escaping () -> Void) {
if isFirstConnectingGamepad() {
AlertControllerUtil.showAlert(
in: vc,
title: "Tips".localized,
message: "controllerNavigationTip".localized,
withCancel: false,
buttonTitle: "Got it!".localized,
countdown: 6,
completion: {
handler()
}
)
}
}
@objc public static func isFirstStreamingOnMac() -> Bool {
if !PublicUtils.isRunningOnMacAsiPadApp {return false}
let key = "hasStreamedOnMac"
+2 -2
View File
@@ -10,9 +10,9 @@ import UIKit
extension UIViewController {
var hasNoPresentedVC: Bool {
let hasPresentedVC = self.presentedViewController == nil && self.view.window != nil
let hasNoPresentedVC = self.presentedViewController == nil && self.view.window != nil
// let presentedVC = self.presentedViewController
return hasPresentedVC
return hasNoPresentedVC
}
}
@@ -99,6 +99,9 @@ private final class ProfileCollectionViewCell: UICollectionViewCell, ControllerN
}
}
override var canBecomeFocused: Bool {
return false
}
}
@objc(FileOperation)
@@ -0,0 +1,370 @@
//
// SettingsTouchVelocityPreview.swift
// VoidLink
//
// Created by True砖家 on 2026/7/30.
// Copyright © 2026 True砖家 on Bilibili. All rights reserved.
//
import ObjectiveC
import UIKit
private var touchVelocityPreviewViewKey: UInt8 = 0
private var touchVelocityPreviewDismissWorkItemKey: UInt8 = 0
private let touchVelocityPreviewScreenAspectRatio: CGFloat = 1.3333
@available(iOS 13.0, *)
private final class TouchVelocityPreviewView: UIView {
static let horizontalScreenInset: CGFloat = 16
static let topPadding: CGFloat = 7
static let titleHeight: CGFloat = 22
static let titleScreenGap: CGFloat = 2
static let footerGap: CGFloat = 6
static let footerHeight: CGFloat = 40
static let bottomPadding: CGFloat = 2
static let heightExtra = topPadding
+ titleHeight
+ titleScreenGap
+ footerGap
+ footerHeight
+ bottomPadding
- horizontalScreenInset * 2 / touchVelocityPreviewScreenAspectRatio
static let preferredWidth: CGFloat = PublicUtils.isIPhone ? 280 : 360
static let preferredHeight = preferredWidth / touchVelocityPreviewScreenAspectRatio + heightExtra
var dividerPercent: CGFloat = 50 {
didSet { setNeedsDisplay() }
}
var velocityPercent: CGFloat = 100 {
didSet { setNeedsDisplay() }
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
isOpaque = false
isUserInteractionEnabled = true
layer.cornerRadius = 14
layer.cornerCurve = .continuous
layer.masksToBounds = true
layer.borderWidth = 1
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(openUrl))
addGestureRecognizer(tapGesture)
}
@objc private func openUrl() {
PublicUtils.openUrl("https://www.bilibili.com/video/BV1A1421r7qA")
}
override func draw(_ rect: CGRect) {
let isDark = ThemeManager.userInterfaceStyle() == .dark
let backgroundColor = isDark
? UIColor(white: 0.07, alpha: 0.9)
: UIColor.white.withAlphaComponent(0.94)
let borderColor = ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.34 : 0.24)
let lineColor = ThemeManager.textColor.withAlphaComponent(isDark ? 0.32 : 0.22)
let passthroughColor = UIColor.systemTeal.withAlphaComponent(isDark ? 0.22 : 0.16)
let velocityColor = UIColor.systemIndigo.withAlphaComponent(isDark ? 0.20 : 0.13)
let controlColor = ThemeManager.textColor.withAlphaComponent(isDark ? 0.82 : 0.66)
let accentColor = ThemeManager.appPrimaryColor
let secondaryAccentColor = UIColor(red: 0.18, green: 0.82, blue: 0.95, alpha: 1)
layer.borderColor = borderColor.cgColor
guard let context = UIGraphicsGetCurrentContext() else { return }
context.setFillColor(backgroundColor.cgColor)
context.fill(bounds)
let titleRect = CGRect(
x: 12,
y: Self.topPadding,
width: bounds.width - 24,
height: Self.titleHeight
)
drawPreviewTitle(in: titleRect, textColor: ThemeManager.textColor)
let screenTop = titleRect.maxY + Self.titleScreenGap
let availableScreenHeight = max(bounds.height - screenTop - Self.footerGap - Self.footerHeight - Self.bottomPadding, 1)
let availableScreenWidth = bounds.width - Self.horizontalScreenInset * 2
let screenWidth = min(availableScreenWidth, availableScreenHeight * touchVelocityPreviewScreenAspectRatio)
let screenHeight = screenWidth / touchVelocityPreviewScreenAspectRatio
let screenRect = CGRect(
x: bounds.midX - screenWidth / 2,
y: screenTop,
width: screenWidth,
height: screenHeight
)
let screenPath = UIBezierPath(roundedRect: screenRect, cornerRadius: 10)
context.saveGState()
screenPath.addClip()
let dividerX = screenRect.minX + min(max(dividerPercent, 0), 100) / 100 * screenRect.width
context.setFillColor(passthroughColor.cgColor)
context.fill(CGRect(x: screenRect.minX, y: screenRect.minY, width: dividerX - screenRect.minX, height: screenRect.height))
context.setFillColor(velocityColor.cgColor)
context.fill(CGRect(x: dividerX, y: screenRect.minY, width: screenRect.maxX - dividerX, height: screenRect.height))
drawDirectionWheel(in: screenRect, color: controlColor, accentColor: secondaryAccentColor)
drawCharacterSelector(in: screenRect, color: controlColor, accentColor: accentColor)
drawActionButtons(in: screenRect, color: controlColor, accentColor: accentColor)
context.restoreGState()
screenPath.lineWidth = 1
lineColor.setStroke()
screenPath.stroke()
context.saveGState()
context.setLineDash(phase: 0, lengths: [6, 5])
context.setStrokeColor(accentColor.withAlphaComponent(0.9).cgColor)
context.setLineWidth(2)
context.move(to: CGPoint(x: dividerX, y: screenRect.minY + 2))
context.addLine(to: CGPoint(x: dividerX, y: screenRect.maxY - 2))
context.strokePath()
context.restoreGState()
drawRegionLabels(screenRect: screenRect, dividerX: dividerX, textColor: ThemeManager.textColor)
drawPreviewFooter(in: CGRect(x: 12, y: screenRect.maxY + Self.footerGap, width: bounds.width - 24, height: Self.footerHeight), textColor: ThemeManager.textColor)
}
private func drawPreviewTitle(in rect: CGRect, textColor: UIColor) {
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = .center
paragraph.lineBreakMode = .byTruncatingTail
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 14.5, weight: .semibold),
.foregroundColor: textColor.withAlphaComponent(0.88),
.paragraphStyle: paragraph
]
("Enhanced Touch Control for Genshin Game Streaming".localized as NSString).draw(in: rect, withAttributes: attributes)
}
private func drawPreviewFooter(in rect: CGRect, textColor: UIColor) {
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = .center
paragraph.lineBreakMode = .byTruncatingTail
let firstLineAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 13, weight: .semibold),
.foregroundColor: textColor.withAlphaComponent(0.82),
.paragraphStyle: paragraph
]
("Originally designed & coded by True Zhuanjia with pride".localized as NSString).draw(
in: CGRect(x: rect.minX, y: rect.minY, width: rect.width, height: 20),
withAttributes: firstLineAttributes
)
let secondLine = "Tap to learn more".localized
let secondLineAttributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 13, weight: .semibold),
.foregroundColor: textColor.withAlphaComponent(0.82),
.paragraphStyle: paragraph
]
(secondLine as NSString).draw(
in: CGRect(x: rect.minX, y: rect.minY + 20, width: rect.width, height: 20),
withAttributes: secondLineAttributes
)
}
private func drawRegionLabels(screenRect: CGRect, dividerX: CGFloat, textColor: UIColor) {
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = .center
paragraph.lineBreakMode = .byTruncatingTail
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 13, weight: .semibold),
.foregroundColor: textColor.withAlphaComponent(0.82),
.paragraphStyle: paragraph
]
let leftRect = CGRect(
x: screenRect.minX + 8,
y: screenRect.minY + 10,
width: max(dividerX - screenRect.minX - 16, 0),
height: 18
)
let rightRect = CGRect(
x: dividerX + 8,
y: screenRect.minY + 10,
width: max(screenRect.maxX - dividerX - 16, 0),
height: 18
)
if leftRect.width >= 72 {
("passthrough".localized as NSString).draw(in: leftRect, withAttributes: attributes)
}
if rightRect.width >= 80 {
let velocityText = LocalizationHelper.localizedString(forKey: "%.0f%% velocity", velocityPercent)
(velocityText as NSString).draw(in: rightRect, withAttributes: attributes)
}
}
private func drawDirectionWheel(in rect: CGRect, color: UIColor, accentColor: UIColor) {
let base = min(rect.width, rect.height)
let center = CGPoint(x: rect.minX + rect.width * 0.18, y: rect.maxY - base * 0.20)
let radius = base * 0.13
color.withAlphaComponent(0.18).setStroke()
let outer = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
outer.lineWidth = 2
outer.stroke()
accentColor.withAlphaComponent(0.9).setStroke()
let arc = UIBezierPath(arcCenter: center, radius: radius * 0.72, startAngle: -.pi * 0.8, endAngle: -.pi * 0.18, clockwise: true)
arc.lineWidth = 5
arc.lineCapStyle = .round
arc.stroke()
color.withAlphaComponent(0.52).setStroke()
let hub = UIBezierPath(ovalIn: CGRect(x: center.x - radius * 0.17, y: center.y - radius * 0.17, width: radius * 0.34, height: radius * 0.34))
hub.lineWidth = 2
hub.stroke()
}
private func drawCharacterSelector(in rect: CGRect, color: UIColor, accentColor: UIColor) {
let rowWidth = rect.width * 0.14
let rowHeight = rect.height * 0.07
let startX = rect.maxX - rowWidth - rect.width * 0.05
let startY = rect.minY + rect.height * 0.22
for index in 0..<3 {
let y = startY + CGFloat(index) * rowHeight * 1.42
let lineRect = CGRect(x: startX, y: y + rowHeight * 0.36, width: rowWidth * 0.6, height: rowHeight * 0.18)
let line = UIBezierPath(roundedRect: lineRect, cornerRadius: lineRect.height / 2)
color.withAlphaComponent(0.34).setFill()
line.fill()
let portraitRect = CGRect(x: startX + rowWidth * 0.68, y: y, width: rowHeight, height: rowHeight)
color.withAlphaComponent(0.38).setStroke()
let portrait = UIBezierPath(ovalIn: portraitRect)
portrait.lineWidth = 2
portrait.stroke()
}
}
private func drawActionButtons(in rect: CGRect, color: UIColor, accentColor: UIColor) {
let base = rect.width
let center = CGPoint(x: rect.maxX - base * 0.19, y: rect.maxY - base * 0.16)
let buttons: [(CGPoint, CGFloat)] = [
(center, base * 0.055),
(CGPoint(x: center.x + base * 0.11, y: center.y - base * 0.077), base * 0.05),
(CGPoint(x: center.x + base * 0.11, y: center.y + base * 0.068), base * 0.05),
(CGPoint(x: center.x - base * 0.11, y: center.y + base * 0.068), base * 0.05)
]
for (index, button) in buttons.enumerated() {
let rect = CGRect(x: button.0.x - button.1, y: button.0.y - button.1, width: button.1 * 2, height: button.1 * 2)
let path = UIBezierPath(ovalIn: rect)
color.withAlphaComponent(index == 0 ? 0.44 : 0.32).setStroke()
path.lineWidth = index == 0 ? 3 : 2
path.stroke()
let iconRect = rect.insetBy(dx: button.1 * 0.42, dy: button.1 * 0.42)
let icon = UIBezierPath(ovalIn: iconRect)
(index == 0 ? accentColor : color).withAlphaComponent(index == 0 ? 0.78 : 0.55).setFill()
icon.fill()
}
}
}
@available(iOS 13.0, *)
extension SettingsViewController {
private var touchVelocityPreviewView: TouchVelocityPreviewView? {
get {
objc_getAssociatedObject(self, &touchVelocityPreviewViewKey) as? TouchVelocityPreviewView
}
set {
objc_setAssociatedObject(self, &touchVelocityPreviewViewKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
private var touchVelocityPreviewDismissWorkItem: DispatchWorkItem? {
get {
objc_getAssociatedObject(self, &touchVelocityPreviewDismissWorkItemKey) as? DispatchWorkItem
}
set {
objc_setAssociatedObject(self, &touchVelocityPreviewDismissWorkItemKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
@objc(showTouchVelocityPreviewWithDividerPercent:velocityPercent:)
func showTouchVelocityPreview(dividerPercent: CGFloat, velocityPercent: CGFloat) {
PublicUtils.runOnMain { [weak self] in
self?.showTouchVelocityPreviewOnMain(dividerPercent: dividerPercent, velocityPercent: velocityPercent)
}
}
private func showTouchVelocityPreviewOnMain(dividerPercent: CGFloat, velocityPercent: CGFloat) {
let previewView = touchVelocityPreviewView ?? TouchVelocityPreviewView()
touchVelocityPreviewView = previewView
previewView.dividerPercent = dividerPercent
previewView.velocityPercent = velocityPercent
previewView.translatesAutoresizingMaskIntoConstraints = false
if previewView.superview == nil {
previewView.alpha = 0
guard let containerView = view.window ?? view else { return }
containerView.addSubview(previewView)
let containerSafeArea = containerView.safeAreaLayoutGuide
var contraints = [
previewView.centerYAnchor.constraint(equalTo: containerSafeArea.centerYAnchor),
previewView.widthAnchor.constraint(equalToConstant: TouchVelocityPreviewView.preferredWidth),
previewView.heightAnchor.constraint(equalToConstant: TouchVelocityPreviewView.preferredHeight)
]
if PublicUtils.isIPhone {
contraints.append(previewView.trailingAnchor.constraint(equalTo: containerSafeArea.trailingAnchor, constant: previewView.bounds.width-50))
}
else {
contraints.append(previewView.centerXAnchor.constraint(equalTo: containerSafeArea.centerXAnchor, constant: 80))
}
NSLayoutConstraint.activate(contraints)
}
previewView.superview?.bringSubviewToFront(previewView)
UIView.animate(withDuration: 0.12) {
previewView.alpha = 1
}
}
@objc(cancelTouchVelocityPreviewDismiss)
func cancelTouchVelocityPreviewDismiss() {
PublicUtils.runOnMain { [weak self] in
self?.touchVelocityPreviewDismissWorkItem?.cancel()
self?.touchVelocityPreviewDismissWorkItem = nil
}
}
@objc(scheduleTouchVelocityPreviewDismiss)
func scheduleTouchVelocityPreviewDismiss() {
PublicUtils.runOnMain { [weak self] in
self?.scheduleTouchVelocityPreviewDismissOnMain()
}
}
private func scheduleTouchVelocityPreviewDismissOnMain() {
guard let previewView = touchVelocityPreviewView else { return }
touchVelocityPreviewDismissWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self, weak previewView] in
UIView.animate(withDuration: 0.18, animations: {
previewView?.alpha = 0
}, completion: { _ in
previewView?.removeFromSuperview()
if self?.touchVelocityPreviewView === previewView {
self?.touchVelocityPreviewView = nil
}
})
}
touchVelocityPreviewDismissWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: workItem)
}
}
@@ -2411,9 +2411,17 @@ BOOL isCustomResolution(int resolutionSelected) {
// pointer veloc setting, will be enable/disabled by touchMode
[self.pointerVelocityModeDividerSlider addTarget:self action:@selector(pointerVelocityModeDividerSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
if (@available(iOS 13.0, *)) {
[self.pointerVelocityModeDividerSlider addTarget:self action:@selector(cancelTouchVelocityPreviewDismiss) forControlEvents:UIControlEventTouchDown];
[self.pointerVelocityModeDividerSlider addTarget:self action:@selector(scheduleTouchVelocityPreviewDismiss) forControlEvents:(UIControlEventTouchUpInside | UIControlEventTouchUpOutside | UIControlEventTouchCancel)];
}
// init pointer veloc setting, will be enable/disabled by touchMode
[self.touchPointerVelocityFactorSlider addTarget:self action:@selector(touchPointerVelocityFactorSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
if (@available(iOS 13.0, *)) {
[self.touchPointerVelocityFactorSlider addTarget:self action:@selector(cancelTouchVelocityPreviewDismiss) forControlEvents:UIControlEventTouchDown];
[self.touchPointerVelocityFactorSlider addTarget:self action:@selector(scheduleTouchVelocityPreviewDismiss) forControlEvents:(UIControlEventTouchUpInside | UIControlEventTouchUpOutside | UIControlEventTouchCancel)];
}
// async native touch event
// self.asyncNativeTouchPrioritySelector.selectedSegmentIndex = currentSettings.asyncNativeTouchPriority.intValue; // load old setting of asyncNativeTouchPriority
@@ -3264,10 +3272,20 @@ BOOL isCustomResolution(int resolutionSelected) {
- (void) pointerVelocityModeDividerSliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" | %d%% | %d%% | ", (uint8_t)sender.value, 100-(uint8_t)sender.value];
bool hasNoPresentedVC = self.presentedViewController == nil && self.view.window != nil;
if (@available(iOS 13.0, *)) if(!settingsViewJustExpanded && hasNoPresentedVC) {
[self showTouchVelocityPreviewWithDividerPercent:sender.value
velocityPercent:[self map_velocFactorDisplay_fromSliderValue:self.touchPointerVelocityFactorSlider.value]];
}
}
- (void) touchPointerVelocityFactorSliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)[self map_velocFactorDisplay_fromSliderValue:sender.value]]; // Update label display
bool hasNoPresentedVC = self.presentedViewController == nil && self.view.window != nil;
if (@available(iOS 13.0, *)) if(!settingsViewJustExpanded && hasNoPresentedVC) {
[self showTouchVelocityPreviewWithDividerPercent:self.pointerVelocityModeDividerSlider.value
velocityPercent:[self map_velocFactorDisplay_fromSliderValue:sender.value]];
}
}
- (void) gyroSensitivitySliderMoved:(UISlider* )sender {
@@ -93,6 +93,7 @@ import UIKit
private var controllerNavigationExecuteDismissWindowOpen = false
private var controllerNavigationExecuteDismissToken = 0
private var controllerNavigationExecuteSuppressNextRelease = false
private var pendingSpecialEntryToken = 0
private var isEditingMode = false {
didSet {
if !isEditingMode {
@@ -643,10 +644,18 @@ import UIKit
private func activateEntry(at indexPath: IndexPath, dismissAfterAction: Bool = true) {
guard entries.indices.contains(indexPath.item) else { return }
let entryIdentifier = entries[indexPath.item].orderIdentifier
activateEntry(withIdentifier: entryIdentifier, fallbackIndexPath: indexPath, dismissAfterAction: dismissAfterAction)
}
switch entries[indexPath.item] {
private func activateEntry(withIdentifier identifier: String, fallbackIndexPath: IndexPath? = nil, dismissAfterAction: Bool = true) {
guard let item = entries.firstIndex(where: { $0.orderIdentifier == identifier }) else { return }
let indexPath = IndexPath(item: item, section: 0)
pendingSpecialEntryToken += 1
switch entries[item] {
case .special(let id, _):
collectionView.deselectItem(at: indexPath, animated: false)
collectionView.deselectItem(at: fallbackIndexPath ?? indexPath, animated: false)
if !isEditingMode {
flashCell(at: indexPath)
handleSpecialEntry(id: id)
@@ -670,6 +679,7 @@ import UIKit
sendKeyboardCommand(command)
if dismissAfterAction && !viewPinned {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
guard self.presentingViewController != nil else { return }
self.dismiss(animated: false)
}
}
@@ -848,7 +858,11 @@ import UIKit
}
private func handleSpecialEntry(id: String) {
pendingSpecialEntryToken += 1
let token = pendingSpecialEntryToken
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
guard self.pendingSpecialEntryToken == token else { return }
self.dismiss(animated: false)
switch id {
@@ -1074,7 +1088,7 @@ extension ToolboxViewController: ControllerCollectionNavigationDelegate {
controllerNavigationDeleteDoublePressToken += 1
let token = controllerNavigationDeleteDoublePressToken
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
guard let self,
self.controllerNavigationDeleteDoublePressToken == token else {
return
@@ -1383,7 +1397,7 @@ private final class ToolboxCardCell: UICollectionViewCell, ControllerNavigationH
}
override var canBecomeFocused: Bool {
return true
return false
}
var controllerNavigationHighlightTargetView: UIView {