Files
VoidLink/VoidLink/Input/OnScreenWidgetView.swift
T

4453 lines
202 KiB
Swift

//
// OnScreenWidgetView.swift
// VoidLink
//
// Created by True砖家 on 2024/8/4.
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
//
import UIKit
import SVGKit
import ObjectiveC.runtime
@objc class OnScreenWidgetView: UIView {
@objc(widgetWithCmdString:buttonLabel:shape:profile:)
class func widget(cmdString: String, buttonLabel: String, shape: String, profile: OSCProfile) -> OnScreenWidgetView {
return OnScreenWidgetView(cmdString: cmdString, buttonLabel: buttonLabel, shape: shape, profile: profile)
}
@objc public static var mapping: [Int16:OnScreenWidgetView] = [:]
@objc public static var isRestoringFolderStates: Bool = false
@objc public static var deferSlideGestureDueToAutoDockRestore: Bool = false
@objc public static var deferScreenEdgeSysGesturesDueToOnScreenWidgets: Bool = false
@objc public static var autoDockRestoreInitByViewResize: Bool = false
@objc public static func set(widget:OnScreenWidgetView, for key:Int16) {
mapping[key] = widget
}
@objc public static func widgetFor(key:Int16) -> OnScreenWidgetView? {
return mapping[key]
}
@objc public static func removeWidgetFromMappings(key:Int16) {
mapping.removeValue(forKey: key)
}
@objc public static func clearMappings() {
mapping.removeAll()
}
@objc public static var unfoldedExclusiveFolderSequence:Int16 = -1
@objc public static var postExclusiveUnfoldedSequences:Set<Int16> = Set()
@objc public static func setPostExclusiveUnfoldeds(_ sequences:NSSet){
let sequenceSet:Set<Int16> = sequences as? Set<Int16> ?? Set()
postExclusiveUnfoldedSequences = Set(sequenceSet)
// print("postExclusiveUnfoldedSequences \(postExclusiveUnfoldedSequences) \(CACurrentMediaTime())")
}
@objc public weak var layoutUpdateDelegate: OnScreenWidgetLayoutUpdateDelegate?
@objc protocol OnScreenWidgetLayoutUpdateDelegate: AnyObject {
func updateGuidelinesForOnScreenWidget(_ sender: Any)
}
@objc public var motionHandler: MotionHandler?
@objc public weak var functionalWidgetDelegate: OnScreenFunctionalWidgetDelegate?
@objc protocol OnScreenFunctionalWidgetDelegate: AnyObject {
func expandSettingsView()
func disconnectRemoteSession()
func disconnectAndQuitApp()
func enterPip()
func bringUpToolboxMenu()
func openWidgetLayoutTool()
func openWidgetProfileTable(pickProfile: Bool)
func bringUpSoftKeyboard()
func alterAbsTouchDragWith(mouseButton:Int32)
func switchPencilHover()
func enablePencilHover()
func disablePencilHover()
func setAllowSingleTouchEnabled(_ enabled:Bool)
func replaceBrush(shortcut:String)
func replaceEraser(shortcut:String)
func presentPressureCurveVC()
func toggleTouch(disabled:Bool)
func toggleGamepadOverlay(overlayEnabled:Bool)
@objc(magnifierMoveStreamViewWithTranslation:)
func magnifierMoveStreamView(translation: CGVector)
@objc(magnifierMoveStreamViewWithTranslation:pinchDelta:)
func magnifierMoveStreamView(translation: CGVector, pinchDelta: CGFloat)
func setMagnifierViewportInteractionEnabled(_ enabled: Bool)
func resetMagnifierStreamView(animated:Bool)
@objc(restoreMagnifierStreamViewWithOffset:scale:)
func restoreMagnifierStreamView(offset: CGPoint, scale: CGFloat)
}
@objc enum WidgetTypeEnum: UInt8 {
case uninitialized
case button
case touchPad
}
private static let MinAutotapInterval:Int = 50
// private let oscProfileMan: OSCProfilesManager = OSCProfilesManager.sharedManager(CGRectZero)
@objc public var oscProfile: OSCProfile
@objc public var widgetType: WidgetTypeEnum = WidgetTypeEnum.uninitialized
@objc static public var editMode: Bool = false
@objc static public var buttonVisualFeedbackEnabled: Bool = true
@objc public var widgetLabel: String
private var nonEditableWidgetLabel: String = ""
@objc public var cmdString: String
@objc public var sequence: Int16 = -1
private var buttonString: String = ""
@objc var functionalButtonString: String = ""
public var motionControlButtonString: String = ""
@objc public var touchPadString: String = ""
// super combo key string set
@objc public var comboButtonStrings: [String] = []
private var comboKeyTimeIntervalMs: UInt32 = 0
@objc public var logicallyDown: Bool = false
@objc public var isBeingResized: Bool = false
@objc public var widthFactor: CGFloat = 1.0 {
didSet{
isBeingResized = true
self.resizeWidgetView()
if OnScreenWidgetView.editMode {self.highlightBorderDuringResizing()}
}
}
@objc public var heightFactor: CGFloat = 1.0 {
didSet{
isBeingResized = true
self.resizeWidgetView()
if OnScreenWidgetView.editMode {self.highlightBorderDuringResizing()}
}
}
private func highlightBorderDuringResizing() {
if widgetType == .touchPad {
self.highlightBorder(highlighted: true, color: standardHighlightColor.cgColor)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
if !self.isBeingResized {
self.highlightBorder(highlighted: false)
}
}
}
}
@objc public var componentSizeFactor: CGFloat = 2.88 {
didSet{
if self.isStickWheel {
self.setupStickWheelLayers()
}
if self.isDisplacementBasedStickPad {
stickThumbSize = self.getDiameter(lengthFactor: self.componentSizeFactor)/4
self.denormalizedComponentSizeFactor = stickThumbSize*4/baselineDiameter
stickAnchorLayer.removeFromSuperlayer()
stickThumb.removeFromSuperlayer()
self.showStickIndicator()
self.updateStickIndicator()
}
}
}
@objc public var buttonMode: ButtonMode = .slideToToggle
@objc private var tapToToggleFlag: Bool = true
@objc public var sizeReference: Int = WidgetSizeReference.longSide.rawValue
@objc public var baselineDiameter:CGFloat = 0
private var baselineWidth:CGFloat = 0
private var baselineHeight:CGFloat = 0
private var baselineWidthLargeSquare:CGFloat = 0
private var baselineHeightLargeSquare:CGFloat = 0
@objc public var denormalizedWidthFactor: CGFloat = 1.0
@objc public var denormalizedHeightFactor: CGFloat = 1.0
@objc public var denormalizedComponentSizeFactor: CGFloat = 1.0
@objc public var borderWidth: CGFloat = 0.0
@objc public var backgroundAlpha: CGFloat = 0.5
@objc public var originalBackgroundAlpha: CGFloat = 0.5{
didSet {
if self.isDisplacementBasedStickPad {
self.stickAnchorLayer.removeFromSuperlayer()
self.stickThumb.removeFromSuperlayer()
self.showStickIndicator()
self.updateStickIndicator()
}
}
}
@objc public var componentAlpha: CGFloat = 1
@objc public var labelAlpha: CGFloat = 0.82
@objc public var originalLabelAlpha: CGFloat = 0.82
@objc public var borderAlpha: CGFloat = 0.1
@objc public var highlightAlpha: CGFloat = 0.77
@objc public var vibrationStyle: Int = 6
@objc public var latestTouchLocation: CGPoint
@objc public var selfViewOnTheRight: Bool = false
@objc public var shape: String = "default"
@objc public var storedCenter: CGPoint = .zero // location from persisted data
@objc public var initialCenter: CGPoint = .zero // location from persisted data
@objc public var layoutChanges: [CGPoint] = []
@objc public var mouseButtonAction: MouseButtonAction = .noClick;
@objc public var mouseButtonActionDelay: TimeInterval = 0.005;
//autoTapTimer
@objc public var autoTapInterval: Int = 49;
@objc public var autoTapRepeats: UInt32 = 0;
private var autoTapCount: UInt32 = 0;
private var autoTapTimer: SafeTimer?
private let appWindow: UIView
private var vibrationGenerator = UIImpactFeedbackGenerator(style: .light)
private var vibrationOn: Bool = false
private var inertialScroller:InertialScroller
@objc public var displayLinkRate:CGFloat = 60;
// for movable buttons during streaming
@objc public var relocatedDuringStreaming: Bool = false
@objc static var profileChangedDuringStreaming: Bool = false
// for all touchPad or buttons hybrid with touchPads
@objc public var hasMinStickOffset: Bool = false
@objc public var hasStickIndicatorOffset: Bool = false
@objc public var isDisplacementBasedStickPad: Bool = false
@objc public var hasAnchorMode: Bool = false
@objc public var hasDisplacementBasedStickPad: Bool = false {
didSet {
if hasDisplacementBasedStickPad {
self.sensitivityXMin = 0.17
self.sensitivityXMax = 1.1
self.sensitivityYMin = 0.17
self.sensitivityYMax = 1.1
}
}
}
@objc public var hasComponent: Bool = false
@objc public var hasSensitivityX: Bool = false
@objc public var sensitivityXMin: CGFloat = 0
@objc public var sensitivityXMax: CGFloat = 8
@objc public var hasSensitivityY: Bool = false
@objc public var sensitivityYMin: CGFloat = 0
@objc public var sensitivityYMax: CGFloat = 8
@objc public var hasSlideThreshold: Bool = false
@objc public var slideThresholdMin: CGFloat = 0
@objc public var slideThresholdMax: CGFloat = 20
@objc public var hasYawFactor: Bool = false
@objc public var yawFactorMin: CGFloat = -1
@objc public var yawFactorMax: CGFloat = 1
@objc public var hasPitchFactor: Bool = false
@objc public var pitchFactorMin: CGFloat = -1
@objc public var pitchFactorMax: CGFloat = 1
@objc public var hasRollFactor: Bool = false
@objc public var rollFactorMin: CGFloat = -1
@objc public var rollFactorMax: CGFloat = 1
@objc public var hasAutoTap: Bool = false
@objc public var isMousePadWithButtonActions: Bool = false
@objc public var hasInertia: Bool = false
@objc public var isFunctionalButton: Bool = false
@objc public var isTapToToggleException: Bool = false
@objc public var hasHapticFeedback: Bool = false
@objc public var isDirectionPad: Bool = false
@objc public var hasWalkSprintKeys: Bool = false
@objc public var hasL3R3Indicator: Bool = false
@objc public var isStickWheel: Bool = false
@objc public var isFolder: Bool = false
@objc public var containsShortcutAction: Bool = false
@objc public var hasNonEditableLabel: Bool = false
@objc public var isMagnifier: Bool = false
@objc public var animatesTransition: Bool = true
// for all stick pads
@objc public var minStickOffset: CGFloat = 0
public let stickMaxOffset: CGFloat = 0x7FFE
public var stickOffsetVector: CGVector = CGVector(dx: 0, dy: 0)
// for LSVPAD, RSVPAD
@objc public var deltaX: CGFloat
@objc public var deltaY: CGFloat
private let VectorStickFactor = 1.5167
private var weightedDeltaX: Int = 0
private var weightedDeltaY: Int = 0
private var weightedOffsetX: CGFloat = 0
private var weightedOffsetY: CGFloat = 0
// shared touch-pad offsets/state
@objc public var offsetX: CGFloat
@objc public var offsetY: CGFloat
@objc var touchPointAnchored: Bool = false {
didSet {
// guard OnScreenWidgetView.editMode else {return}
if self.isDisplacementBasedStickPad {
self.hasStickIndicatorOffset = touchPointAnchored
self.stickAnchorLayer.removeFromSuperlayer()
self.stickThumb.removeFromSuperlayer()
self.showStickIndicator()
self.updateStickIndicator()
}
}
}
@objc public var l3r3Indicator = CAShapeLayer()
// for LSPAD, RSPAD
@objc public var stickThumb = CAShapeLayer()
@objc public var stickAnchorLayer = CAShapeLayer()
// LSWHEEL/RSWHEEL
@objc public var stickWheelLayer = CALayer()
@objc public var stickWheelLayerSmall = CALayer()
@objc public var stickWheelAxis = CALayer()
@objc public var dWheelWalkModeThreshold: CGFloat
// this is for all stick pads and mouse Pad
@objc public var sensitivityFactorX: CGFloat = 1.0 {
didSet {
guard OnScreenWidgetView.editMode else {return}
if self.hasDisplacementBasedStickPad {
self.stickAnchorLayer.removeFromSuperlayer()
self.stickThumb.removeFromSuperlayer()
self.showStickIndicator()
self.updateStickIndicator()
}
}
}
@objc public var sensitivityFactorY: CGFloat = 1.0
@objc public var yawFactor: CGFloat = 1.0
@objc public var pitchFactor: CGFloat = 1.0
@objc public var rollFactor: CGFloat = 1.0
private var gyroControlPreviousStatus: NSMutableDictionary = NSMutableDictionary()
private var superViewWidth: CGFloat = 0
private var superViewHeight: CGFloat = 0
private var absMousePaused: Bool = false
// check quick double tap:
private var quickDoubleTapDetected: Bool
private var temporarilyMovable: Bool = false
private var touchTapTimeInterval: TimeInterval
private var touchTapTimeStamp: TimeInterval
private var QUICK_TAP_TIME_INTERVAL = 0.2
@objc public var stickIndicatorOffset: CGFloat = 120 {
didSet {
if self.isDisplacementBasedStickPad {
self.updateStickIndicator()
}
}
}
// for all LRUD pads
@objc public enum WalkSprintKeyActionType: UInt8 {
case hold
case toggle
}
@objc public var keyboardDpadThresholdRef: CGFloat = 300
@objc public var walkKeyThreshold: CGFloat = 0.08
@objc public var walkKeyActionType: WalkSprintKeyActionType = .hold
@objc public var sprintKeyThreshold: CGFloat = 0.6
@objc public var sprintKeyActionType: WalkSprintKeyActionType = .hold
@objc public var upIndicator = CAShapeLayer()
@objc public var downIndicator = CAShapeLayer()
@objc public var leftIndicator = CAShapeLayer()
@objc public var rightIndicator = CAShapeLayer()
@objc public var sprintSign = CALayer()
@objc public var walkSign = CALayer()
@objc public var walkKeyThresholdPreviewLayer = CAShapeLayer()
@objc public var sprintKeyThresholdPreviewLayer = CAShapeLayer()
// for DPAD LRUD pad
@objc public var anchorBall = CAShapeLayer()
private let triggeringAngle = 67.5
private enum Direction: Int {
case right = 1
case up = 2
case left = 4
case down = 8
case initialStatus = 16
}
private var previousButtonMask = Direction.initialStatus.rawValue
// OnScreenControls instance
@objc public var onScreenControls: OnScreenControls?
// key / button label
private let label: UILabel
// first touch location within the button or pad view (self)
@objc public var touchBeganLocation: CGPoint = .zero
// for mousePad
private var touchBegan: Bool = false
private var directionPadTouchBegan: Bool = false
@objc private(set) var firstTouchMoved: Bool = false
private var mousePointerMoved: Bool
private var twoTouchesDetected: Bool
private var allSpawnedTouchesCount: Int = 0
// trackball
private var trackballVelocity: CGPoint = .zero
private var trackballDecelerationTimer: Timer?
@objc public var decelerationRateX: CGFloat = 0.5
@objc public var decelerationRateY: CGFloat = 0.5
private let trackballVelocityThreshold: CGFloat = 0.1
@objc public var slideThreshold: CGFloat = 6.0
// border & visual effect
private var minimumBorderAlpha: CGFloat = 0.19
private var defaultBorderColor: CGColor = UIColor(white: 0.2, alpha: 0.3).cgColor
// private let voidlinkPurple: CGColor = UIColor(red: 0.5, green: 0.5, blue: 1.0, alpha: 0.86).cgColor
private let standardHighlightColor: UIColor = UIColor(
red: 0.46,
green: 0.74,
blue: 0.98,
alpha: 0.92
)
//slide buttons
private var capturedTouches: NSMutableSet
let setLock = NSLock()
//controller touch pad
private var pointerIdPool: Set<UInt32>
private var pointerIdDict: Dictionary<ObjectIdentifier, UInt32>
private var activePointerIds: Set<UInt32>
// whole button press down visual effect
@objc public let buttonDownVisualEffectLayer = CAShapeLayer()
@objc public var buttonDownVisualEffectStandardWidth: CGFloat
@objc public var highlightSizeFactor: CGFloat = 1.0
@objc static public var isTweakingHighlight: Bool = false
// discreteWheels
private var tickCycle: UInt8 = UIScreen.main.maximumFramesPerSecond > 110 ? 20 : 10
private var tickFlag: UInt8 = 0
@objc public var folded: Bool = false
@objc public var persistedFolded: Bool = false
@objc public var revealMode: RevealMode = .coexist
@objc public var bulkMoveEnabled: Bool = false
@objc public var sequenceSet: Set<Int16> = Set()
@objc public var parentSequence: Int16 = -1
@objc public var standardFoldingInterval: TimeInterval = 0.05
static weak var capturer: OnScreenWidgetView?
@objc static weak var deepestButton: OnScreenWidgetView?
@objc init(cmdString: String, buttonLabel: String, shape:String, profile:OSCProfile) {
self.cmdString = cmdString
self.touchPadString = ""
if !self.cmdString.contains("+"){
// 安全解包并处理 `comboKeyStrings`
if let comboStrings = CommandManager.shared.extractCmdStrings(from: self.cmdString) {
if CommandManager.touchPadCmds.contains(comboStrings.first ?? "") {self.widgetType = WidgetTypeEnum.touchPad}
else {self.widgetType = WidgetTypeEnum.button}
let touchPadString = Set(comboStrings).intersection(Set(CommandManager.touchPadCmds)).first ?? ""
self.comboButtonStrings = comboStrings.filter{$0 != touchPadString}
self.touchPadString = touchPadString
self.buttonString = self.comboButtonStrings.first ?? ""
self.functionalButtonString = Set(self.comboButtonStrings).intersection(Set(CommandManager.functionalButtonCmds)).first ?? ""
self.motionControlButtonString = Set(self.comboButtonStrings).intersection(Set(CommandManager.motionControlButtonCmds)).first ?? ""
switch self.cmdString {
case "LSPAD", "LSVPAD", "LSWHEEL":
self.comboButtonStrings = ["OSCL3"]
case "RSPAD", "RSVPAD", "RSWHEEL":
self.comboButtonStrings = ["OSCR3"]
case "DS4TOUCH":
self.comboButtonStrings = ["DS4TCHBTN"]
default: break
}
}
else {print("无法从 keyString 提取 comboKeyStrings")}
}
else {
self.widgetType = WidgetTypeEnum.button // legacy combo button connected by "+"
}
// print("widgetType: \(self.widgetType)")
// print("touchPadString: \(self.touchPadString)")
for comboButtonString in comboButtonStrings {
// print("comboButtonString: \(comboButtonString)")
}
self.widgetLabel = buttonLabel
self.shape = shape
self.label = UILabel()
// self.originalBackgroundColor = UIColor(white: 0.2, alpha: 0.7)
// self.widthFactor = 1.0
// self.heightFactor = 1.0
// self.backgroundAlpha = 0.5
// self.velocityFactor = 1.0
self.latestTouchLocation = CGPoint(x: 0, y: 0)
self.deltaX = 0
self.deltaY = 0
self.offsetX = 0
self.offsetY = 0
self.onScreenControls = OnScreenControls()
self.appWindow = UIApplication.shared.windows.first!
self.quickDoubleTapDetected = false
self.touchTapTimeInterval = 100
self.touchTapTimeStamp = 100
self.buttonDownVisualEffectStandardWidth = 0
self.mousePointerMoved = false
self.twoTouchesDetected = false
self.stickIndicatorOffset = 95
self.sensitivityFactorX = 1.0
self.sensitivityFactorY = 1.0
self.capturedTouches = NSMutableSet()
self.pointerIdDict = [:]
self.pointerIdPool = []
for i in 0...10 { // iPadOS supports up to 11 finger touches
self.pointerIdPool.insert(UInt32(i))
}
self.activePointerIds = []
self.oscProfile = profile
self.inertialScroller = InertialScroller()
dWheelWalkModeThreshold = stickMaxOffset*0.2
// self.motionHandler = MotionHandler.shared(profile: profile)
super.init(frame: .zero)
// helps widget panel to hide/show stacks
self.accessWidgetAttributes()
if self.widgetType == WidgetTypeEnum.button {
if !self.touchPadString.isEmpty {
self.mouseButtonAction = MouseButtonAction.noClick
self.buttonMode = .regular
}
if !self.motionControlButtonString.isEmpty {
self.buttonMode = .tapToToggle
}
if self.cmdString.contains("BRUSH"){
self.functionalButtonString = "BRUSH"
}
if self.cmdString.contains("ERASER"){
self.functionalButtonString = "ERASER"
}
if self.cmdString.contains("FOLDER") {
self.functionalButtonString = "FOLDER"
}
if self.isFunctionalButton {
self.buttonMode = .movable
if self.functionalButtonString == "PENCILHOVER" {self.buttonMode = .regular}
}
}
if self.widgetType == .touchPad {
if self.isStickWheel {
self.widthFactor = 2
self.heightFactor = 2.6
self.backgroundAlpha = 1
self.originalBackgroundAlpha = 1
self.componentAlpha = self.backgroundAlpha
self.borderAlpha = 0.05
self.sensitivityFactorX = 0.42
self.sensitivityFactorY = 0.42
self.componentSizeFactor = 2.8
}
if self.isDisplacementBasedStickPad {
self.widthFactor = 1.8
self.heightFactor = 1.8
self.backgroundAlpha = 0.43
self.originalBackgroundAlpha = 0.43
self.componentAlpha = self.backgroundAlpha
self.borderAlpha = 0.05
self.sensitivityFactorX = 0.6
self.sensitivityFactorY = 0.6
self.componentSizeFactor = 0.85
}
if self.isDirectionPad {
self.widthFactor = 2
self.heightFactor = 2
self.sensitivityFactorX = 0
self.sensitivityFactorY = 0
}
}
if self.widgetType == .button {
if self.isFolder || self.isFunctionalButton {
self.widthFactor = GenericUtils.isIPhone() ? 0.88 : 1.17
self.heightFactor = GenericUtils.isIPhone() ? 0.56 : 0.77
}
}
self.tweakBorderAlpha(alpha: self.borderAlpha) // fix default borderAlpha offset
self.onScreenControls = OnScreenControls.shared()
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc public func accessWidgetAttributes(){
self.hasMinStickOffset = (CommandManager.stickTouchPads.contains(self.touchPadString)
|| CommandManager.stickWheels.contains(self.touchPadString))
self.hasDisplacementBasedStickPad = CommandManager.displacementBasedStickPads.contains(self.touchPadString)
self.isDisplacementBasedStickPad = self.hasDisplacementBasedStickPad && widgetType == WidgetTypeEnum.touchPad
self.hasStickIndicatorOffset = isDisplacementBasedStickPad && touchPointAnchored
self.hasSensitivityX = CommandManager.touchPadCmds.contains(self.touchPadString) && !CommandManager.verticalTouchPads.contains(self.touchPadString)
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){
self.sensitivityYMin = -4.0
self.sensitivityYMax = 4.0
}
if CommandManager.mousePads.contains(self.touchPadString){
self.sensitivityXMin = 0
self.sensitivityXMax = 16.0
self.sensitivityYMin = 0
self.sensitivityYMax = 16.0
}
self.hasYawFactor = self.motionControlButtonString == "GYRO" && (oscProfile.mapGyroTo == .mapGyroToMouse || oscProfile.yawPitchToRightStick)
self.hasPitchFactor = self.hasYawFactor
self.yawFactorMin = -1.0
self.yawFactorMax = 1.0
self.pitchFactorMin = -1.0
self.pitchFactorMax = 1.0
self.hasRollFactor = self.motionControlButtonString == "GYRO" && (oscProfile.mapGyroTo == .mapGyroToControllerStick && oscProfile.rollToLeftStick)
self.rollFactorMin = -1.0
self.rollFactorMax = 1.0
self.hasAutoTap = self.widgetType == WidgetTypeEnum.button && self.functionalButtonString == "" && self.motionControlButtonString == ""
self.isMousePadWithButtonActions = CommandManager.mousePadWithButtonActions.contains(self.touchPadString) && widgetType == WidgetTypeEnum.touchPad
self.hasInertia = CommandManager.inertialTouchPads.contains(self.touchPadString)
self.isFunctionalButton = self.functionalButtonString != "" || self.cmdString.contains("+")
self.isTapToToggleException = (self.functionalButtonString == "NOSINGLETOUCH"
|| self.functionalButtonString == "PENCILHOVER"
|| self.functionalButtonString == "ABSTCHDRAG"
)
self.hasHapticFeedback = !self.comboButtonStrings.isEmpty || CommandManager.directionPads.contains(self.touchPadString)
self.isDirectionPad = self.widgetType == WidgetTypeEnum.touchPad && CommandManager.directionPads.contains(self.touchPadString)
self.hasWalkSprintKeys = self.isDirectionPad && (self.touchPadString == "WASDPAD"
|| self.touchPadString == "ARROWPAD")
self.isStickWheel = self.widgetType == WidgetTypeEnum.touchPad && CommandManager.stickWheels.contains(self.touchPadString)
self.isFolder = self.cmdString.contains("FOLDER")
self.containsShortcutAction = self.cmdString.contains("+")
self.hasComponent = self.isStickWheel || (self.isDisplacementBasedStickPad && !self.touchPointAnchored)
self.hasL3R3Indicator = !self.isStickWheel && !self.isDirectionPad && self.widgetType == WidgetTypeEnum.touchPad
/*
self.hasTrackPoint = (CommandManager.vectorTouchPads.contains(self.touchPadString)
|| self.isStickWheel
|| (self.widgetType == WidgetTypeEnum.button
&& (buttonMode == .slideAndHold || buttonMode == .slideToToggle)))*/
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;
self.isMagnifier = self.cmdString.contains("MAGNIFIER")
self.hasAnchorMode = isDisplacementBasedStickPad || isDirectionPad
}
// ======================================================================================================
@objc public func setupAutoTapTimer() {
if self.widgetType == WidgetTypeEnum.button, autoTapInterval >= OnScreenWidgetView.MinAutotapInterval {
self.autoTapTimer = SafeTimer(interval:0.001 * Double(autoTapInterval)) {
// print("timer instance \(Unmanaged.passUnretained(self.autoTapTimer!).toOpaque()), \(CACurrentMediaTime())")
if self.autoTapRepeats > 0 {
self.autoTapCount += 1
if self.autoTapCount >= self.autoTapRepeats {
self.autoTapTimer?.pause()
if self.buttonMode == .tapToToggle {
self.tapToToggleFlag = !self.tapToToggleFlag
}
}
}
self.handleButtonDown()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.025) {
self.handleButtonUp()
}
}
}
}
@objc public func setupInertialScroller(fps: Int) {
if self.hasInertia {
self.inertialScroller = InertialScroller(decelerationRate: self.decelerationRateX, displayLinkRate: CGFloat(fps))
self.inertialScroller.decelerationRateY = self.decelerationRateY
}
}
@objc public func setVibration(style: Int) {
if #available(iOS 13.0, *) {
vibrationOn = style < UIImpactFeedbackGenerator.FeedbackStyle.rigid.rawValue + 1
} else {
vibrationOn = style < UIImpactFeedbackGenerator.FeedbackStyle.heavy.rawValue + 1
};
vibrationStyle = style;
if #available(iOS 13.0, *) {
print("rigid value \(UIImpactFeedbackGenerator.FeedbackStyle.rigid.rawValue)")
} else {
// Fallback on earlier versions
};
if vibrationOn {
vibrationGenerator = UIImpactFeedbackGenerator(style: UIImpactFeedbackGenerator.FeedbackStyle(rawValue: style) ?? UIImpactFeedbackGenerator.FeedbackStyle.light)
}
}
@objc public func setLocation(position: CGPoint) {
/*
NSLayoutConstraint.activate([
self.centerXAnchor.constraint(equalTo: self.superview!.leadingAnchor, constant: xOffset),
self.centerYAnchor.constraint(equalTo: self.superview!.topAnchor, constant: yOffset),
])
*/
storedCenter = position
center = storedCenter
initialCenter = storedCenter;
layoutChanges.append(initialCenter)
}
@objc public func enableRelocationMode(enabled: Bool){
OnScreenWidgetView.editMode = enabled
}
@objc public func undoRelocation(){
guard layoutChanges.count>1 else {return}
UIView.animate(withDuration: 0.2) {
self.center = self.layoutChanges[self.layoutChanges.count-2]
}
storedCenter = center
layoutChanges.removeLast()
}
@objc public func adjustTransparency(alpha: CGFloat, tweakBorderAlpha:Bool){
CATransaction.begin()
CATransaction.setDisableActions(true)
self.backgroundAlpha = alpha
self.originalBackgroundAlpha = alpha
if self.hasComponent {
self.componentAlpha = self.backgroundAlpha
self.tweakAlpha(tweakBorderAlpha: false)
if self.isStickWheel {self.setupStickWheelLayers()}
}
else{
self.tweakAlpha(tweakBorderAlpha: tweakBorderAlpha)
}
CATransaction.commit()
}
@objc public func adjustBorder(width: CGFloat){
self.borderWidth = width
self.layer.borderWidth = borderWidth
// if CommandManager.touchPadCmds.contains(self.keyString) && width == 0 {self.layer.borderWidth = 1}
if self.shape == "round" {
//setup round buttons
self.layer.cornerRadius = self.frame.width/2
// self.layer.borderWidth = self.borderWidth
label.minimumScaleFactor = 0.15 // Adjust the scale factor for oscButtons
}
}
@objc public func resizeWidgetView(){
guard let superview = superview else { return }
CATransaction.begin()
CATransaction.setDisableActions(true)
// Deactivate existing constraints if necessary
NSLayoutConstraint.deactivate(self.constraints)
// To resize the button, we must set this to false temporarily
translatesAutoresizingMaskIntoConstraints = false
// replace invalid factor values
// if self.widthFactor == 0 {self.widthFactor = 1.0}
// if self.heightFactor == 0 {self.heightFactor = 1.0}
/*
NSLayoutConstraint.activate([
self.centerXAnchor.constraint(equalTo: self.superview!.leadingAnchor, constant: storedLocation.x),
self.centerYAnchor.constraint(equalTo: self.superview!.topAnchor, constant: storedLocation.y)])
*/
// Constraints for resizing
self.changeAndActivateContraints()
// Trigger layout update
superview.layoutIfNeeded()
// Re-setup widgetView style
setupView()
updateMovementThresholdPreview()
CATransaction.commit()
}
@objc public func resizeComponent(){
CATransaction.begin()
CATransaction.setDisableActions(true)
if widgetType == WidgetTypeEnum.touchPad, CommandManager.stickWheels.contains(touchPadString) {
setupStickWheelLayers()
}
CATransaction.commit()
}
@objc public func tweakLabelAlpha(alpha:CGFloat){
labelAlpha = alpha
originalLabelAlpha = alpha
// label.textColor = UIColor(white: 1.0, alpha: labelAlpha)
self.setupAtrributedText()
}
@objc public func tweakBorderAlpha(alpha:CGFloat){
borderAlpha = alpha
defaultBorderColor = UIColor(white: borderAlpha>0 ? 0.1 : 0.9, alpha: abs(borderAlpha)).cgColor
self.layer.borderColor = defaultBorderColor
}
@objc public func tweakHighlightAlpha(alpha:CGFloat){
highlightAlpha = alpha
self.buttonDownVisualEffectLayer.borderColor = standardHighlightColor.withAlphaComponent(highlightAlpha).cgColor
self.l3r3Indicator.borderColor = self.buttonDownVisualEffectLayer.borderColor
self.anchorBall.borderColor = self.buttonDownVisualEffectLayer.borderColor
self.upIndicator.borderColor = self.buttonDownVisualEffectLayer.borderColor
self.downIndicator.borderColor = self.buttonDownVisualEffectLayer.borderColor
self.leftIndicator.borderColor = self.buttonDownVisualEffectLayer.borderColor
self.rightIndicator.borderColor = self.buttonDownVisualEffectLayer.borderColor
self.walkKeyThresholdPreviewLayer.strokeColor = self.buttonDownVisualEffectLayer.borderColor
self.sprintKeyThresholdPreviewLayer.strokeColor = self.buttonDownVisualEffectLayer.borderColor
GraphicUtils.changeColor(layer: self.sprintSign, color: standardHighlightColor.withAlphaComponent(highlightAlpha))
GraphicUtils.changeColor(layer: self.walkSign, color: standardHighlightColor.withAlphaComponent(highlightAlpha))
}
private func tweakAlpha(tweakBorderAlpha:Bool, tweakLabelAlpha:Bool = true){
// setup default border from self.backgroundAlpha
let realBackgroundAlpha = max(abs(self.backgroundAlpha) - 0.18, 0) // offset to be consistent with legacy onScreen controller layer opacity
self.backgroundColor = UIColor(white:self.backgroundAlpha>0 ? 0.1 : 0.9, alpha: realBackgroundAlpha) // offset to be consistent with legacy onScreen controller layer opacity
if tweakBorderAlpha {
borderAlpha = realBackgroundAlpha * 1.005 * (self.backgroundAlpha>0 ? 1 : -1)
defaultBorderColor = UIColor(white: borderAlpha>0 ? 0.1 : 0.9, alpha: abs(borderAlpha)).cgColor
self.layer.borderColor = defaultBorderColor
}
if tweakLabelAlpha {self.tweakLabelAlpha(alpha: backgroundAlpha > 0 ? 0.8 : -0.92)}
if widgetType == WidgetTypeEnum.touchPad {
self.backgroundColor = UIColor.clear // make touchPad transparent
}
}
func nearestEven(_ value: CGFloat) -> CGFloat {
let rounded = round(value)
if Int(rounded) % 2 == 0 {
return rounded
} else {
let lowerEven = rounded - 1
let upperEven = rounded + 1
return abs(value - lowerEven) <= abs(value - upperEven) ? lowerEven : upperEven
}
}
// 仅在加载控件时调用
private func denormalizeSize(sizeFactor:CGFloat) -> CGFloat {
let longSideLen = max(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
let shortSideLen = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
var referenceLen = UIScreen.main.bounds.width
if(self.sizeReference == WidgetSizeReference.longSide.rawValue) {referenceLen = longSideLen}
if(self.sizeReference == WidgetSizeReference.shortSide.rawValue) {referenceLen = shortSideLen}
// return CGFloat(Int(sizeFactor/10000*screenWidthInPoints/2)*2);
// print("referenceLen \(referenceLen), \(CACurrentMediaTime())")
return nearestEven(sizeFactor/10000*referenceLen);
}
private func getBaselineLenths(){
let longSideLen = max(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
let shortSideLen = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
baselineDiameter = self.sizeReference == WidgetSizeReference.longSide.rawValue ? 60 : 60*shortSideLen/longSideLen
baselineWidth = self.sizeReference == WidgetSizeReference.longSide.rawValue ? 70 : 70*shortSideLen/longSideLen
baselineHeight = self.sizeReference == WidgetSizeReference.longSide.rawValue ? 65 : 65*shortSideLen/longSideLen
baselineWidthLargeSquare = self.sizeReference == WidgetSizeReference.longSide.rawValue ? 170 : 170*shortSideLen/longSideLen
baselineHeightLargeSquare = self.sizeReference == WidgetSizeReference.longSide.rawValue ? 150 : 150*shortSideLen/longSideLen
}
private func getDiameter(lengthFactor:CGFloat) -> CGFloat {
self.getBaselineLenths()
let isNormalizedSizeFactor = lengthFactor > 10;
return isNormalizedSizeFactor ? denormalizeSize(sizeFactor:lengthFactor) : CGFloat(Int(baselineDiameter * lengthFactor / 2) * 2)
}
private func getRecSize(widthFactor:CGFloat, heightFactor:CGFloat) -> CGSize {
let isNormalizedSizeFactor = widthFactor > 10;
let isNormalizedHeightFactor = heightFactor > 10;
self.getBaselineLenths()
let width = isNormalizedSizeFactor ? denormalizeSize(sizeFactor:widthFactor) : CGFloat(Int(baselineWidth * widthFactor / 2) * 2)
let height = isNormalizedHeightFactor ? denormalizeSize(sizeFactor:heightFactor) : CGFloat(Int(baselineHeight * heightFactor / 2) * 2)
return CGSize(width:width, height: height)
}
private func getLargeRecSize(widthFactor:CGFloat, heightFactor:CGFloat) -> CGSize {
let isNormalizedSizeFactor = self.widthFactor > 10;
let isNormalizedHeightFactor = self.heightFactor > 10;
self.getBaselineLenths()
let width = isNormalizedSizeFactor ? denormalizeSize(sizeFactor:self.widthFactor) : CGFloat(Int(baselineWidthLargeSquare * self.widthFactor / 2) * 2)
let height = isNormalizedHeightFactor ? denormalizeSize(sizeFactor:self.heightFactor) : CGFloat(Int(baselineHeightLargeSquare * self.heightFactor / 2) * 2)
return CGSize(width:width, height: height)
}
private func changeAndActivateContraints(){
if self.shape == "round"{ // we'll make custom osc buttons round & smaller
let diameter = getDiameter(lengthFactor: self.widthFactor)
NSLayoutConstraint.activate([
self.widthAnchor.constraint(equalToConstant: diameter),
self.heightAnchor.constraint(equalToConstant: diameter),])
// 实时调整大小时isNormalized 为 false。加载数据时 isNormalized 为 true
// baselineDiameter 仅在 实时调整大小时生效,从存储恢复时总是会恢复denormalizeSize()尺寸
self.denormalizedWidthFactor = diameter/baselineDiameter;
self.denormalizedHeightFactor = diameter/baselineDiameter;
//此处的 deNormalized 用于slider显示值
}
if self.shape == "square" {
let widgetSize = getRecSize(widthFactor: self.widthFactor, heightFactor: self.heightFactor)
autoDockOriginalBoundsSize = widgetSize
NSLayoutConstraint.activate([
self.widthAnchor.constraint(equalToConstant: widgetSize.width),
self.heightAnchor.constraint(equalToConstant: widgetSize.height),])
self.denormalizedWidthFactor = widgetSize.width/baselineWidth;
self.denormalizedHeightFactor = widgetSize.height/baselineHeight;
}
if self.shape == "largeSquare" { // override all shape strings
let widgetSize = getLargeRecSize(widthFactor: self.widthFactor, heightFactor: self.heightFactor)
NSLayoutConstraint.activate([
self.widthAnchor.constraint(equalToConstant:widgetSize.width),
self.heightAnchor.constraint(equalToConstant:widgetSize.height),])
self.denormalizedWidthFactor = widgetSize.width/baselineWidthLargeSquare;
self.denormalizedHeightFactor = widgetSize.height/baselineHeightLargeSquare;
}
NSLayoutConstraint.activate(self.shape == "round" ? [
label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 13), // set up label size contrain within UIView
label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -13),
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.centerYAnchor.constraint(equalTo: centerYAnchor),]
: [
label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10), // set up label size contrain within UIView
label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10),
label.topAnchor.constraint(equalTo: self.topAnchor, constant: 8), // set up label size contrain within UIView
label.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8),
label.centerXAnchor.constraint(equalTo: centerXAnchor),
label.centerYAnchor.constraint(equalTo: centerYAnchor),])
if self.shape != "round"{
self.setSquareWidgetCornerRadius()
}
}
private func automaticSquareButtonCornerRadius(for size: CGSize) -> CGFloat {
let shortSideLen = min(size.width, size.height)
let longSideLen = max(size.width, size.height)
let aspectRatio = longSideLen / max(shortSideLen, 1)
let aspectProgress = min(max((aspectRatio - 1) / 1.6, 0), 1)
let radiusRatio = 0.34 - aspectProgress * 0.08
let minRadius: CGFloat = min(16, shortSideLen/2)
let maxRadius: CGFloat = 30
return min(max(shortSideLen * radiusRatio, minRadius), maxRadius)
}
private func setSquareWidgetCornerRadius(){
let boundsSize = self.layer.bounds.size
let shortSideLen = min(boundsSize.width, boundsSize.height)
if self.widgetType == .button, self.shape == "square" {
self.layer.cornerRadius = automaticSquareButtonCornerRadius(for: boundsSize)
} else {
self.layer.cornerRadius = shortSideLen/2 < 16 ? shortSideLen/3.2 : 16
}
if #available(iOS 13.0, *) {
self.layer.cornerCurve = .continuous
}
}
func containsNonLatin(_ text: String) -> Bool {
for scalar in text.unicodeScalars {
if !(0x0000...0x024F).contains(scalar.value) {
return true
}
}
return false
}
@objc func reverseColorPhase(reversed: Bool){
backgroundAlpha = reversed ? (originalBackgroundAlpha.sign == .minus ? 0.5 : -0.5) : originalBackgroundAlpha
self.tweakAlpha(tweakBorderAlpha: true, tweakLabelAlpha: false)
labelAlpha = reversed ? (originalBackgroundAlpha.sign == .minus ? 0.5 : -0.9) : originalLabelAlpha
self.setupAtrributedText()
}
@objc func setupAtrributedText(){
var text = self.widgetLabel.contains("#") ? "\(self.widgetLabel.split(separator: "#").first ?? "")" : LocalizationHelper.localizedString(forKey: self.widgetLabel)
if !OnScreenWidgetView.editMode, self.widgetType == .touchPad {
text = ""
}
if self.hasNonEditableLabel {
switch cmdString {
case "DISABLETOUCH":
self.nonEditableWidgetLabel = LocalizationHelper.localizedString(forKey: touchDisabledFLag ? "=EnableTouch" : "=DisableTouch" )
case "GAMEPADOVERLAY":
self.nonEditableWidgetLabel = LocalizationHelper.localizedString(forKey: OnScreenWidgetView.gamepadOverlayFLag ? "=GamepadOverlayOn" : "=GamepadOverlayOff" )
default:
nonEditableWidgetLabel = ""
}
text = self.nonEditableWidgetLabel
}
if self.isDisplacementBasedStickPad {
text = ""
}
let attr = NSAttributedString(
string: self.folded ? "[\(text)]" : (self.isFolder ? " 🟡 \(text)" : "\(text)"),
attributes: [
.foregroundColor: UIColor(white:labelAlpha>0 ? 1.0 : 0, alpha: abs(labelAlpha)), // 填充色
.strokeColor: (labelAlpha>0 ? UIColor.black : UIColor.white).withAlphaComponent(abs(labelAlpha)*0.43), // 描边色
.strokeWidth: widgetType == .touchPad ? 7 : (containsNonLatin(text) ? -1 : -4) // 负值 = 同时填充
]
)
label.attributedText = attr
label.textAlignment = .center
label.baselineAdjustment = .alignCenters
}
private func setupView() {
// label.text = self.widgetLabel
// label.font = UIFont.boldSystemFont(ofSize: 19)
// label.font = UIFont.systemFont(ofSize: 19, weight: .medium, design: .rounded)
let baseFont = UIFont.boldSystemFont(ofSize: self.shape == "round" ? 22 : 19)
if #available(iOS 13.0, *) {
if let desc = baseFont.fontDescriptor.withDesign(.rounded) {
label.font = UIFont(descriptor: desc, size: 0)
} else {
label.font = baseFont
}
} else {
label.font = baseFont
}
label.translatesAutoresizingMaskIntoConstraints = false
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.1 // Adjust the scale factor as needed
label.textAlignment = .center
// label.textColor = UIColor(white: 1.0, alpha: labelAlpha)
// label.shadowColor = .black
// label.shadowOffset = CGSize(width: 0, height: 0)
self.setupAtrributedText()
label.translatesAutoresizingMaskIntoConstraints = false // enable auto alignment for the label
label.isHidden = shouldHideStandardLabel()
self.translatesAutoresizingMaskIntoConstraints = true // this is mandatory to prevent unexpected key view location change
self.layer.borderWidth = self.borderWidth
self.tweakAlpha(tweakBorderAlpha: false, tweakLabelAlpha: false)
if self.shape == "default" || self.shape.isEmpty {
if CommandManager.oscButtonMappings.keys.contains(self.buttonString) && !CommandManager.oscRectangleButtonCmds.contains(self.buttonString){ //make oscButtons round
self.shape = "round"
}
else {
self.shape = "square"
}
}
if self.widgetType == WidgetTypeEnum.touchPad {
self.shape = "largeSquare" // override shape from user input
// if(self.borderWidth < 1) {self.layer.borderWidth = 1}
// else {self.layer.borderWidth = self.borderWidth}
self.layer.borderWidth = self.borderWidth
if OnScreenWidgetView.editMode { //display label in edit mode to make the pad more visible
// label.text = self.widgetLabel
if CommandManager.stickWheels.contains(self.touchPadString) {label.isHidden = true}
}
else{
label.isHidden = self.widgetLabel.uppercased() == self.touchPadString // allow touchPad label to be display if it's different from touchPad cmdString
}
}
if !self.functionalButtonString.isEmpty{
self.layer.borderWidth = self.borderWidth
}
if self.shape == "round" {
//setup round buttons
self.layer.cornerRadius = self.frame.width/2
label.minimumScaleFactor = 0.15 // Adjust the scale factor for oscButtons
}
if self.shape == "square" || self.shape == "largeSquare" {
self.setSquareWidgetCornerRadius()
}
// self.layer.shadowColor = UIColor.clear.cgColor
// self.layer.shadowRadius = 8
// self.layer.shadowOpacity = 0.5
addSubview(label)
_ = installCustomContentIfNeeded()
if(OnScreenWidgetView.editMode) {self.changeAndActivateContraints()}
center = storedCenter //anchor the center while resizing self
hideAllHighlightLayersOfAllWidgets(selfIncluded: false)
if widgetType == WidgetTypeEnum.button {setupButtonDownVisualEffectLayer()}
if CommandManager.directionPads.contains(touchPadString) {setupLrudDirectionIndicatorlayers()}
if CommandManager.stickTouchPads.contains(touchPadString) {setupL3R3Indicator()}
if CommandManager.verticalTouchPads.contains(touchPadString) {setupL3R3Indicator()}
if CommandManager.mousePadWithButtonActions.contains(self.touchPadString) {setupL3R3Indicator()}
if self.isStickWheel {
self.setupStickWheelLayers()
}
if self.isDisplacementBasedStickPad{
self.stickAnchorLayer.removeFromSuperlayer()
self.stickThumb.removeFromSuperlayer()
self.showStickIndicator()
self.updateStickIndicator()
}
if self.isDirectionPad {
self.setupLrudDirectionIndicatorlayers()
}
if self.isFolder {
QUICK_TAP_TIME_INTERVAL = 0.15
}
}
@objc func shouldHideStandardLabel() -> Bool {
false
}
@discardableResult
@objc func installCustomContentIfNeeded() -> Bool {
false
}
@objc func handleQuickDoubleTapAction() -> Bool {
false
}
@objc var hasTrackPoint: Bool = false
@objc static var trackPointEnabled: Bool = false
private var trackPointMapping: [UITouch:CAShapeLayer] = [:]
private var trackPointPool:Set<CAShapeLayer> = Set()
/// stickWheel
private func
setupStickWheelLayers(){
let diameter = self.getDiameter(lengthFactor: self.componentSizeFactor)
self.denormalizedComponentSizeFactor = diameter/baselineDiameter
// let tintColor = UIColor(red: 0.5, green: 0.5, blue: 1.0, alpha: 1)
let tintColor = UIColor(
red: 0x48 / 255.0,
green: 0xF5 / 255.0,
blue: 0xFF / 255.0,
alpha: componentAlpha
)
let axisdiameter = self.getDiameter(lengthFactor: UIDevice.current.userInterfaceIdiom == .phone ? 0.35 : 0.5)
let axisSize = CGSize(width: axisdiameter, height: axisdiameter)
self.stickWheelAxis.removeFromSuperlayer()
self.stickWheelAxis = GraphicUtils.makeSVGLayer(from: "StickWheelAxis", in: self.layer, targetSize: axisSize)
self.layer.insertSublayer(self.stickWheelAxis, at: 0)
// GraphicUtils.changeColor(layer: self.stickWheelAxis, color: .white.withAlphaComponent(1))
self.stickWheelAxis.isHidden = false
self.stickWheelLayer.removeFromSuperlayer()
self.stickWheelLayer = GraphicUtils.makeSVGLayer(from: "StickWheel", in: self.layer, targetSize: CGSize(width: diameter, height: diameter))
self.layer.insertSublayer(self.stickWheelLayer, at: 0)
GraphicUtils.changeColor(layer: self.stickWheelLayer, color: tintColor)
self.stickWheelLayer.setAffineTransform(.identity)
self.stickWheelLayer.isHidden = !OnScreenWidgetView.editMode
let smallWheelSize = CGSize(width: diameter*0.56766, height: diameter*0.56766)
self.stickWheelLayerSmall.removeFromSuperlayer()
self.stickWheelLayerSmall = GraphicUtils.makeSVGLayer(from: "StickWheelSmall", in: self.layer, targetSize: smallWheelSize)
self.layer.insertSublayer(self.stickWheelLayerSmall, below: stickWheelLayer)
GraphicUtils.changeColor(layer: self.stickWheelLayerSmall, color: tintColor)
self.stickWheelLayerSmall.setAffineTransform(.identity)
self.stickWheelLayerSmall.isHidden = !OnScreenWidgetView.editMode
}
private func setHiddenForStickWheelLayer(hidden:Bool){
CATransaction.begin()
CATransaction.setDisableActions(true)
self.stickWheelLayer.isHidden = hidden;
self.stickWheelLayerSmall.isHidden = hidden;
CATransaction.commit()
}
private func handleStickWheelMove(touch:UITouch){
CATransaction.begin()
CATransaction.setDisableActions(true)
let isInWalkMode = hypot(stickOffsetVector.dx, stickOffsetVector.dy) < dWheelWalkModeThreshold
let angle = atan2(offsetY, offsetX) + .pi/2
let transform = CGAffineTransform(rotationAngle: angle)
if isInWalkMode {
self.stickWheelLayerSmall.setAffineTransform(transform)
}
else {
self.stickWheelLayer.setAffineTransform(transform)
}
if touch.phase == .began {
self.stickWheelLayer.isHidden = isInWalkMode
self.stickWheelLayerSmall.isHidden = !isInWalkMode
}
else if self.stickWheelLayer.isHidden != isInWalkMode {
self.stickWheelLayer.isHidden = isInWalkMode
self.stickWheelLayerSmall.isHidden = !isInWalkMode
}
CATransaction.commit()
switch self.touchPadString {
case "LSWHEEL":
DispatchQueue.global(qos: .userInteractive).async {
self.weightedDeltaX = 1
self.sendLeftStickTouchPadEvent(weightedTouchX: self.offsetX * self.sensitivityFactorX, weightedTouchY: self.offsetY * self.sensitivityFactorY, circulate: true)
}
case "RSWHEEL":
DispatchQueue.global(qos: .userInteractive).async {
self.weightedDeltaX = 1
self.sendRightStickTouchPadEvent(weightedTouchX: self.offsetX * self.sensitivityFactorX, weightedTouchY: self.offsetY * self.sensitivityFactorY, circulate: true)
}
default:
break
}
}
//=====LRUD(left right up & down buttons) touchPad touch =========================================
private func showLrudBall(at point: CGPoint) {
// Create a circular path using UIBezierPath
CATransaction.begin()
CATransaction.setDisableActions(true)
anchorBall.position = point
anchorBall.isHidden = false;
CATransaction.commit()
}
private func setupLrudBall() {
// Create a circular path using UIBezierPath
let path = UIBezierPath(arcCenter: CGPoint.zero, radius: 10*min(highlightSizeFactor,1.0), startAngle: 0, endAngle: 2 * .pi, clockwise: true)
// Create a CAShapeLayer
anchorBall.path = path.cgPath // Assign the circular path to the shape layer
if anchorBall.superlayer == nil {
self.layer.addSublayer(anchorBall)
}
anchorBall.position = CGPoint(x: self.bounds.width/2, y: self.bounds.height/2,)
// Set the stroke color and width (border of the circle)
anchorBall.strokeColor = stickThumbColor
anchorBall.lineWidth = 0
anchorBall.shadowOffset = CGSize(width: 0.5, height: 0.5)
anchorBall.shadowRadius = 0;
anchorBall.shadowOpacity = 0.8
anchorBall.name = "lrudBall"
if !OnScreenWidgetView.isTweakingHighlight {anchorBall.isHidden = true}
// Set the fill color (inside of the circle)
anchorBall.fillColor = stickThumbColor // Light fill with some transparency
}
private func setupLrudDirectionLayer(directionLayer:CAShapeLayer) {
let indicatorFrame = CAShapeLayer();
indicatorFrame.frame = CGRectMake(0, 0, 75*highlightSizeFactor, 75*highlightSizeFactor)
indicatorFrame.cornerRadius = 23 * highlightSizeFactor
if #available(iOS 13.0, *) {
indicatorFrame.cornerCurve = .continuous
}
directionLayer.borderWidth = 5.3 * min(1.0, highlightSizeFactor)
directionLayer.frame = indicatorFrame.bounds.insetBy(dx: -directionLayer.borderWidth, dy: -directionLayer.borderWidth) // Adjust the inset as needed
directionLayer.cornerRadius = indicatorFrame.cornerRadius + directionLayer.borderWidth
directionLayer.backgroundColor = UIColor.clear.cgColor
directionLayer.fillColor = UIColor.clear.cgColor
let path = UIBezierPath(roundedRect: directionLayer.bounds, cornerRadius: directionLayer.cornerRadius)
directionLayer.path = path.cgPath
directionLayer.borderColor = standardHighlightColor.cgColor
directionLayer.position = CGPoint(x: self.bounds.width/2, y: self.bounds.height/2)
if directionLayer.superlayer == nil {
self.layer.insertSublayer(directionLayer, below: anchorBall)
}
}
private func showLrudDirectionIndicator(with indicatorLayer:CAShapeLayer){
// show the indicator based on the touchBeganLocation
indicatorLayer.position = touchPointAnchored ? touchBeganLocation : CGPoint(x: self.bounds.midX, y: self.bounds.midY)
if indicatorLayer.isHidden {
indicatorLayer.isHidden = false
if vibrationOn {
vibrationGenerator.prepare()
vibrationGenerator.impactOccurred()
}
}
}
private var previousWalkMode:Bool = false
private var previousSprintMode:Bool = false
private func handleLrudTouchMove(){
if touchPointAnchored, !firstTouchMoved {
return
}
CATransaction.begin()
CATransaction.setDisableActions(true)
if self.hasWalkSprintKeys {
let dist = hypot(self.offsetX, self.offsetY)
let isInWalkMode = dist < self.walkKeyThreshold * keyboardDpadThresholdRef * 0.5
let isInSprintMode = dist > self.sprintKeyThreshold * keyboardDpadThresholdRef * 0.5
if (isInSprintMode != previousSprintMode || directionPadTouchBegan)
, self.comboButtonStrings.count>0 {
switch sprintKeyActionType {
case .hold:
if isInSprintMode {sendComboButtonsDownEvent(comboStrings: [comboButtonStrings[0]])}
else {sendComboButtonsUpEvent(comboStrings: [comboButtonStrings[0]])}
case .toggle :
if directionPadTouchBegan {break}
sendComboButtonsDownEvent(comboStrings: [comboButtonStrings[0]])
DispatchQueue.global().asyncAfter(deadline: .now() + 0.025) {
self.sendComboButtonsUpEvent(comboStrings: [self.comboButtonStrings[0]])
}
}
self.sprintSign.isHidden = !isInSprintMode
if !directionPadTouchBegan, isInSprintMode, vibrationOn {
vibrationGenerator.prepare()
vibrationGenerator.impactOccurred()
}
}
if (isInWalkMode != previousWalkMode || directionPadTouchBegan)
, self.comboButtonStrings.count>1 {
switch walkKeyActionType {
case .hold:
if isInWalkMode {sendComboButtonsDownEvent(comboStrings: [comboButtonStrings[1]])}
else {sendComboButtonsUpEvent(comboStrings: [comboButtonStrings[1]])}
case .toggle :
if directionPadTouchBegan {break}
sendComboButtonsDownEvent(comboStrings: [comboButtonStrings[1]])
DispatchQueue.global().asyncAfter(deadline: .now() + 0.025) {
self.sendComboButtonsUpEvent(comboStrings: [self.comboButtonStrings[1]])
}
}
self.walkSign.isHidden = !isInWalkMode
if !directionPadTouchBegan, isInWalkMode, vibrationOn {
vibrationGenerator.prepare()
vibrationGenerator.impactOccurred()
}
}
previousWalkMode = isInWalkMode
previousSprintMode = isInSprintMode
}
let radians = atan2(-offsetY,offsetX)
let degrees = radians * 180 / .pi
let nearZeroPoint = sensitivityFactorX == 0 || sensitivityFactorY == 0 ? false : abs(offsetX) < 16/sensitivityFactorX && abs(offsetY) < 16/sensitivityFactorY
// NSLog("deltaX: %f, detalY: %f", deltaX, deltaY)
if self.stickWheelAxis.isHidden {
self.stickWheelAxis.isHidden = false
}
var pressedButtonMask = 0;
if abs(degrees) < triggeringAngle {
// NSLog("button pressed: right")
pressedButtonMask = pressedButtonMask | Direction.right.rawValue
}
if 180.0 - abs(degrees) < triggeringAngle {
// NSLog("button pressed: left")
pressedButtonMask = pressedButtonMask | Direction.left.rawValue
}
if abs(90.0 - degrees) < triggeringAngle {
// NSLog("button pressed: up")
pressedButtonMask = pressedButtonMask | Direction.up.rawValue
}
if abs(-90.0 - degrees) < triggeringAngle {
// NSLog("button pressed: down")
pressedButtonMask = pressedButtonMask | Direction.down.rawValue
}
if nearZeroPoint {pressedButtonMask = 0}
if pressedButtonMask != previousButtonMask || directionPadTouchBegan {
directionPadTouchBegan = false
if(pressedButtonMask & Direction.up.rawValue == Direction.up.rawValue) {
showLrudDirectionIndicator(with: upIndicator)
switch touchPadString {
case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["W"]!,Int8(KEY_ACTION_DOWN), 0)
case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["UP_ARROW"]!,Int8(KEY_ACTION_DOWN), 0)
case "DPAD": self.onScreenControls?.pressDownControllerButton(UP_FLAG)
default: break
}
}
else{
self.upIndicator.isHidden = true
switch touchPadString {
case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["W"]!,Int8(KEY_ACTION_UP), 0)
case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["UP_ARROW"]!,Int8(KEY_ACTION_UP), 0)
case "DPAD": self.onScreenControls?.releaseControllerButton(UP_FLAG)
default: break
}
}
if(pressedButtonMask & Direction.down.rawValue == Direction.down.rawValue){
showLrudDirectionIndicator(with: downIndicator)
switch touchPadString {
case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["S"]!,Int8(KEY_ACTION_DOWN), 0)
case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["DOWN_ARROW"]!,Int8(KEY_ACTION_DOWN), 0)
case "DPAD": self.onScreenControls?.pressDownControllerButton(DOWN_FLAG)
default: break
}
}
else{
self.downIndicator.isHidden = true
switch touchPadString {
case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["S"]!,Int8(KEY_ACTION_UP), 0)
case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["DOWN_ARROW"]!,Int8(KEY_ACTION_UP), 0)
case "DPAD": self.onScreenControls?.releaseControllerButton(DOWN_FLAG)
default: break
}
}
if(pressedButtonMask & Direction.left.rawValue == Direction.left.rawValue){
showLrudDirectionIndicator(with: leftIndicator)
switch touchPadString {
case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["A"]!,Int8(KEY_ACTION_DOWN), 0)
case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["LEFT_ARROW"]!,Int8(KEY_ACTION_DOWN), 0)
case "DPAD": self.onScreenControls?.pressDownControllerButton(LEFT_FLAG)
default: break
}
}
else{
self.leftIndicator.isHidden = true
switch touchPadString {
case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["A"]!,Int8(KEY_ACTION_UP), 0)
case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["LEFT_ARROW"]!,Int8(KEY_ACTION_UP), 0)
case "DPAD": self.onScreenControls?.releaseControllerButton(LEFT_FLAG)
default: break
}
}
if(pressedButtonMask & Direction.right.rawValue == Direction.right.rawValue){
showLrudDirectionIndicator(with: rightIndicator)
switch touchPadString {
case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["D"]!,Int8(KEY_ACTION_DOWN), 0)
case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["RIGHT_ARROW"]!,Int8(KEY_ACTION_DOWN), 0)
case "DPAD": self.onScreenControls?.pressDownControllerButton(RIGHT_FLAG)
default: break
}
}
else{
self.rightIndicator.isHidden = true
switch touchPadString {
case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["D"]!,Int8(KEY_ACTION_UP), 0)
case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["RIGHT_ARROW"]!,Int8(KEY_ACTION_UP), 0)
case "DPAD": self.onScreenControls?.releaseControllerButton(RIGHT_FLAG)
default: break
}
}
}
previousButtonMask = pressedButtonMask
CATransaction.commit()
}
//================================================================================================
//===== MOUSEPAD related methods=============================================================
private func sendLongMouseLeftButtonClickEvent() {
DispatchQueue.global(qos: .userInteractive).async {
// Logging the press event
NSLog("Sending left mouse button press")
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), BUTTON_LEFT)
// Wait 200 ms to simulate a real button press
DispatchQueue.global().asyncAfter(deadline: .now() + self.QUICK_TAP_TIME_INTERVAL) {
// If quick tap is not detected, release the button
if !self.quickDoubleTapDetected {
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_LEFT)
// NSLog("double click: first long click release")
}
else{NSLog("Left mouse button release cancelled, keep pressing down, turning into dragging...")}
// Don't release the button if we're still dragging, this will prevent the dragging from being interrupted.
}
}
}
private func sendShortMouseLeftButtonClickEvent() {
DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) {
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), BUTTON_LEFT)
DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) {
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_LEFT)
}
}
}
private func sendMouseRightButtonClickEvent() {
DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) {
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), BUTTON_RIGHT)
DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) {
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_RIGHT)
}
}
}
//mousepad-trackball behavior========================================================
private func startTrackballMomentum() {
// stopTrackballMomentum()
if self.inertialScroller.handler == nil {
self.inertialScroller.handler = {
LiSendMouseMoveEvent(
Int16(self.inertialScroller.vector.dx*1.7),
Int16(self.inertialScroller.vector.dy*1.7)
)
}
}
self.inertialScroller.timer?.restart()
}
private func stopTrackballMomentum() {
self.inertialScroller.timer?.pause()
}
//==== Button actions =============================================
//==== Button widget tap down=============================================
private func handleTapDownOrSlidein() {
if self.isHidden {return}
if autoTapInterval < OnScreenWidgetView.MinAutotapInterval {
handleButtonDown()
}
else{
self.autoTapCount = 0
self.autoTapTimer?.restart()
}
}
private func handleFingerUpOrSlideout(leaveNonSkillButtonAlone: Bool = false, event: UIEvent? = nil) {
if self.isHidden {return}
if autoTapInterval < OnScreenWidgetView.MinAutotapInterval {
handleButtonUp(leaveNonSkillButtonAlone:leaveNonSkillButtonAlone, event:event)
}
else{
self.autoTapTimer?.pause()
self.handleButtonUp(leaveNonSkillButtonAlone: leaveNonSkillButtonAlone)
}
}
private func handleButtonDown() {
if !self.isUserInteractionEnabled {return}
if !OnScreenWidgetView.editMode, !self.functionalButtonString.isEmpty {self.handleFunctionalButtonDown()}
if !OnScreenWidgetView.editMode {self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)}
if !OnScreenWidgetView.editMode, !self.motionControlButtonString.isEmpty {self.handleMotionControlButtonDown()}
self.buttonDownVisualEffect()
if vibrationOn {
vibrationGenerator.prepare()
vibrationGenerator.impactOccurred()
}
}
private func buttonDownVisualEffect(){
logicallyDown = true
if OnScreenWidgetView.buttonVisualFeedbackEnabled {
CATransaction.begin()
CATransaction.setDisableActions(true)
if widgetType == WidgetTypeEnum.button {buttonDownVisualEffectLayer.isHidden = false}
CATransaction.commit()
}
}
private func buttonUpVisualEffect(){
logicallyDown = false
if OnScreenWidgetView.buttonVisualFeedbackEnabled {
CATransaction.begin()
CATransaction.setDisableActions(true)
if !OnScreenWidgetView.isTweakingHighlight {buttonDownVisualEffectLayer.isHidden = true}
CATransaction.commit()
}
}
private func handleButtonUp(leaveNonSkillButtonAlone:Bool = false, event: UIEvent? = nil) {
if !self.isUserInteractionEnabled {return}
if !OnScreenWidgetView.editMode {
DispatchQueue.main.asyncAfter(deadline: .now() + (self.hasDisplacementBasedStickPad ? 0.02 : 0)) {
self.sendComboButtonsUpEvent(comboStrings: self.comboButtonStrings)
}
}
if !OnScreenWidgetView.editMode && !self.motionControlButtonString.isEmpty{
self.handleMotionControlButtonUp()
}
self.buttonUpVisualEffect()
if !OnScreenWidgetView.editMode && !self.functionalButtonString.isEmpty{
// print("handleFingerUpOrSlideout leaveNonSkillButtonAlone, \(leaveNonSkillButtonAlone) \(CACurrentMediaTime())")
if !leaveNonSkillButtonAlone {self.handleFunctionalButtonUp(event:event)}
}
// legacy keyboard button combo connected by "+"
if !OnScreenWidgetView.editMode && self.cmdString.contains("+") && !self.cmdString.contains("-"){
if buttonMode == .movable, moveableButtonLongPressed() {return}
if leaveNonSkillButtonAlone {return}
self.buttonDownVisualEffect()
var autoReleaseComboButtons = CommandManager.shared.extractAutoReleaseButtonStrings(from: self.cmdString)!
autoReleaseComboButtons.removeAll{
Set(CommandManager.pencilProButtonCmds).contains($0)
}
CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: autoReleaseComboButtons) // send multi-key command
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.buttonUpVisualEffect()
}
}
}
@objc public func setupLrudDirectionIndicatorlayers() {
CATransaction.begin()
CATransaction.setDisableActions(true)
// self.setupLrudBall()
setupLrudDirectionLayer(directionLayer: upIndicator)
let offset = upIndicator.borderWidth/(2*upIndicator.bounds.width)
upIndicator.anchorPoint = CGPoint(x: 0.5, y: 1-offset)
setupLrudDirectionLayer(directionLayer: downIndicator)
downIndicator.anchorPoint = CGPoint(x: 0.5, y: 0+offset)
setupLrudDirectionLayer(directionLayer: leftIndicator)
leftIndicator.anchorPoint = CGPoint(x: 1-offset, y: 0.5)
setupLrudDirectionLayer(directionLayer: rightIndicator)
rightIndicator.anchorPoint = CGPoint(x: 0+offset, y: 0.5)
if !OnScreenWidgetView.isTweakingHighlight {
leftIndicator.isHidden = true
rightIndicator.isHidden = true
upIndicator.isHidden = true
downIndicator.isHidden = true
}
if !touchPointAnchored || OnScreenWidgetView.editMode {
let axisdiameter = self.getDiameter(lengthFactor: UIDevice.current.userInterfaceIdiom == .phone ? 0.27 : 0.35)
let axisSize = CGSize(width: axisdiameter, height: axisdiameter)
self.stickWheelAxis.removeFromSuperlayer()
self.stickWheelAxis = GraphicUtils.makeSVGLayer(from: "StickWheelAxis-0.75", in: self.layer, targetSize: axisSize)
GraphicUtils.changeColor(layer: self.stickWheelAxis, color: UIColor(white: 1, alpha: 0.5))
self.layer.addSublayer(self.stickWheelAxis)
self.stickWheelAxis.isHidden = false
}
self.sprintSign.removeFromSuperlayer()
self.sprintSign = GraphicUtils.makeSVGLayer(from: "sprintSign", in: self.upIndicator, at:CGPoint(x: 0.5+0.028, y: 0.5-0.22), targetSize: CGSize(width: 26*highlightSizeFactor, height: 26*highlightSizeFactor))
GraphicUtils.changeColor(layer: self.sprintSign, color: standardHighlightColor)
self.upIndicator.addSublayer(self.sprintSign)
var frameInSelfLayer = self.upIndicator.convert(self.sprintSign.frame, to: self.layer)
self.sprintSign.removeFromSuperlayer()
self.layer.addSublayer(self.sprintSign)
self.sprintSign.frame = frameInSelfLayer
self.sprintSign.isHidden = !OnScreenWidgetView.editMode
self.walkSign.removeFromSuperlayer()
self.walkSign = GraphicUtils.makeSVGLayer(from: "walkSign", in: self.upIndicator, at:CGPoint(x: 0.5+0.032, y: 0.5-0.22), targetSize: CGSize(width: 32*highlightSizeFactor, height: 32*highlightSizeFactor))
GraphicUtils.changeColor(layer: self.walkSign, color: standardHighlightColor)
self.upIndicator.addSublayer(self.walkSign)
frameInSelfLayer = self.upIndicator.convert(self.walkSign.frame, to: self.layer)
self.walkSign.removeFromSuperlayer()
self.layer.addSublayer(self.walkSign)
self.walkSign.frame = frameInSelfLayer
self.walkSign.isHidden = true
setupMovementThresholdPreviewLayersIfNeeded()
updateMovementThresholdPreview()
CATransaction.commit()
}
private func setupMovementThresholdPreviewLayersIfNeeded() {
for previewLayer in [sprintKeyThresholdPreviewLayer, walkKeyThresholdPreviewLayer] {
previewLayer.fillColor = UIColor.clear.cgColor
previewLayer.strokeColor = standardHighlightColor.cgColor
previewLayer.lineWidth = 3
previewLayer.lineDashPattern = [8, 6]
previewLayer.isHidden = true
if previewLayer.superlayer == nil {
layer.insertSublayer(previewLayer, below: anchorBall)
}
}
}
private func updateMovementThresholdPreview(layer: CAShapeLayer, threshold: CGFloat) {
let clampedThreshold = max(0, min(1, threshold))
let diameter: CGFloat = keyboardDpadThresholdRef * clampedThreshold
let radius = max(diameter / 2, 1)
layer.position = CGPoint(x: bounds.midX, y: bounds.midY)
layer.path = UIBezierPath(
arcCenter: .zero,
radius: radius,
startAngle: 0,
endAngle: .pi * 2,
clockwise: true
).cgPath
}
@objc public func updateMovementThresholdPreview() {
guard isDirectionPad, touchPadString == "WASDPAD" || touchPadString == "ARROWPAD" else {
walkKeyThresholdPreviewLayer.isHidden = true
sprintKeyThresholdPreviewLayer.isHidden = true
return
}
setupMovementThresholdPreviewLayersIfNeeded()
updateMovementThresholdPreview(layer: sprintKeyThresholdPreviewLayer, threshold: sprintKeyThreshold)
if self.comboButtonStrings.count>1 {updateMovementThresholdPreview(layer: walkKeyThresholdPreviewLayer, threshold: walkKeyThreshold)}
let shouldShow = OnScreenWidgetView.editMode
sprintKeyThresholdPreviewLayer.isHidden = !shouldShow
walkKeyThresholdPreviewLayer.isHidden = !(shouldShow && self.comboButtonStrings.count>1)
}
@objc public func hideAllHighlightLayersOfAllWidgets(selfIncluded:Bool) {
self.forEachWidget(){ widget in
if !selfIncluded && widget == self {return}
widget.buttonDownVisualEffectLayer.isHidden = true;
widget.l3r3Indicator.isHidden = true
widget.anchorBall.isHidden = true
widget.upIndicator.isHidden = true
widget.downIndicator.isHidden = true
widget.leftIndicator.isHidden = true
widget.rightIndicator.isHidden = true
widget.walkKeyThresholdPreviewLayer.isHidden = true
widget.sprintKeyThresholdPreviewLayer.isHidden = true
}
}
@objc public func setupButtonDownVisualEffectLayer() {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.buttonDownVisualEffectStandardWidth = 8
if self.shape == "round" {
if denormalizedWidthFactor < 1.3 {self.buttonDownVisualEffectStandardWidth = 15.3} // wider visual effect for osc buttons
else {self.buttonDownVisualEffectStandardWidth = 9}
}
if self.motionControlButtonString != "" {self.buttonDownVisualEffectStandardWidth = 3}
// Set the frame to be larger than the view to expand outward
buttonDownVisualEffectLayer.borderWidth = CGFloat(Int(self.buttonDownVisualEffectStandardWidth * self.highlightSizeFactor / 2) * 2) // set this 0 to hide the visual effect first
buttonDownVisualEffectLayer.borderColor = standardHighlightColor.cgColor
buttonDownVisualEffectLayer.frame = self.bounds.insetBy(dx: -buttonDownVisualEffectLayer.borderWidth, dy: -buttonDownVisualEffectLayer.borderWidth) // Adjust the inset as needed
buttonDownVisualEffectLayer.cornerRadius = self.layer.cornerRadius + buttonDownVisualEffectLayer.borderWidth
if self.shape == "square" {if #available(iOS 13.0, *) {
buttonDownVisualEffectLayer.cornerCurve = .continuous
}}
buttonDownVisualEffectLayer.backgroundColor = UIColor.clear.cgColor;
buttonDownVisualEffectLayer.fillColor = UIColor.clear.cgColor;
// Create a path for the border
let path = UIBezierPath( roundedRect: buttonDownVisualEffectLayer.bounds, cornerRadius: buttonDownVisualEffectLayer.cornerRadius)
buttonDownVisualEffectLayer.path = path.cgPath
if buttonDownVisualEffectLayer.superlayer == nil {
self.layer.insertSublayer(buttonDownVisualEffectLayer, below: self.layer)
}
buttonDownVisualEffectLayer.position = CGPointMake(self.bounds.midX, self.bounds.midY)
if !OnScreenWidgetView.isTweakingHighlight {buttonDownVisualEffectLayer.isHidden = true}
CATransaction.commit()
}
//==========================================================================================================
//=========================================send on screen controller stick/trigger events
private func sendRightStickTouchPadEvent(weightedTouchX: CGFloat, weightedTouchY: CGFloat, circulate: Bool=false){
let targetX = self.weightedTouchInputToStickOffset(input: weightedTouchX)
let targetY = -self.weightedTouchInputToStickOffset(input: weightedTouchY)
let mixRightStickInputToGyro = (oscProfile.mapGyroTo == .mapGyroToControllerStick
&& oscProfile.yawPitchToRightStick)
if !mixRightStickInputToGyro || (self.motionHandler?.gyroMixInputStarted() != true) {
stickOffsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: targetX, dy: targetY), minOffset: minStickOffset, circulate: circulate)
self.onScreenControls?.sendRightStickTouchPadEvent(stickOffsetVector.dx, stickOffsetVector.dy)
}
self.motionHandler?.mixOnScreenRightStickAndGyroInput(x: targetX, y: targetY)
if !OnScreenWidgetView.gamepadArrivalReported {OnScreenWidgetView.gamepadArrivalReported = true}
}
private func sendLeftStickTouchPadEvent(weightedTouchX:CGFloat, weightedTouchY:CGFloat, circulate:Bool=false){
let targetX = self.weightedTouchInputToStickOffset(input: weightedTouchX)
let targetY = -self.weightedTouchInputToStickOffset(input: weightedTouchY)
let mixLeftStickInputToGyro = (oscProfile.mapGyroTo == .mapGyroToControllerStick
&& oscProfile.rollToLeftStick)
if !mixLeftStickInputToGyro || (self.motionHandler?.gyroMixInputStarted() != true) {
stickOffsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: targetX, dy: targetY), minOffset: minStickOffset, circulate: circulate)
self.onScreenControls?.sendLeftStickTouchPadEvent(stickOffsetVector.dx, stickOffsetVector.dy)
}
self.motionHandler?.mixOnScreenLeftStickAndGyroInput(x: targetX, y: targetY)
if !OnScreenWidgetView.gamepadArrivalReported {OnScreenWidgetView.gamepadArrivalReported = true}
}
private func sendLeftTriggerTouchPadEvent(inputY: CGFloat){
self.onScreenControls?.updateLeftTrigger(UInt8(max(min(inputY,255),0)))
}
private func sendRightTriggerTouchPadEvent(inputY: CGFloat){
self.onScreenControls?.updateRightTrigger(UInt8(max(min(inputY,255),0)))
}
//==========================================================================================================
private func sendOscButtonDownEvent(oscString: String){
let buttonFlag = CommandManager.oscButtonMappings[oscString]
if buttonFlag != 0 {self.onScreenControls?.pressDownControllerButton(buttonFlag!)}
else {switch oscString {
case "OSCL2", "L2", "LT":
self.onScreenControls?.updateLeftTrigger(0xFF)
case "OSCR2", "R2", "RT":
self.onScreenControls?.updateRightTrigger(0xFF)
default:break
}}
}
private func sendOscButtonUpEvent(oscString: String){
let buttonFlag = CommandManager.oscButtonMappings[oscString]
if buttonFlag != 0 {self.onScreenControls?.releaseControllerButton(buttonFlag!)}
else {switch oscString {
case "OSCL2", "L2", "LT":
self.onScreenControls?.updateLeftTrigger(0x00)
case "OSCR2", "R2", "RT":
self.onScreenControls?.updateRightTrigger(0x00)
default:break
}}
}
//==============================================================================
private var hasUnifiedTriggerInterval: Bool {
if let lastString = comboButtonStrings.last, lastString.contains("MS") {
return true
}
else {return false}
}
private func getMilliSecIntervalFrom(intervalString: String?) -> UInt32{
guard let timeIntervalString = intervalString?.replacingOccurrences(of: "MS", with: "") else { return 0 }
if let timeInterval = UInt32(timeIntervalString) {
return timeInterval
}
else {return 0}
}
private func getMilliSecIntervalFrom(cmdString: String) -> UInt32{
if cmdString.contains(".") {
let intervalString = cmdString.split(separator: ".").last.map(String.init)
return getMilliSecIntervalFrom(intervalString: intervalString)
}
else {return 0}
}
private func handleButtonStringDown(_ buttonString: String) {
let realButtonString: String
if buttonString.contains(".") {
realButtonString = buttonString.split(separator: ".").first.map(String.init)!
DispatchQueue.global().asyncAfter(deadline: .now() + Double(getMilliSecIntervalFrom(cmdString: buttonString))/1000) {
self.handleButtonStringUp(realButtonString)
}
}
else {realButtonString = buttonString}
DispatchQueue.global(qos: .userInteractive).async {
if CommandManager.oscButtonMappings.keys.contains(realButtonString) {
self.sendOscButtonDownEvent(oscString: realButtonString)
if !OnScreenWidgetView.gamepadArrivalReported {OnScreenWidgetView.gamepadArrivalReported = true}
}
if CommandManager.keyboardButtonMappings.keys.contains(realButtonString) {
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings[realButtonString]!,Int8(KEY_ACTION_DOWN), 0)
}
if CommandManager.mouseButtonMappings.keys.contains(realButtonString), self.functionalButtonString != "ABSTCHDRAG" {
let button = Int32(CommandManager.mouseButtonMappings[realButtonString]!)
if abs(button) == 0xFF {
LiSendScrollEvent(button>0 ? 3 : -3)
}
else {LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), button)}
}
}
}
private func sendComboButtonsDownEvent(comboStrings: [String]) {
let hasUnifiedTriggerInterval = self.hasUnifiedTriggerInterval
if hasUnifiedTriggerInterval {
self.comboKeyTimeIntervalMs = self.getMilliSecIntervalFrom(intervalString: comboStrings.last!)
}
if #available(iOS 13.0, *) {
Task {
for cmdString in comboStrings {
let isIntervalCmd = cmdString.contains("MS")
let triggerDelay = hasUnifiedTriggerInterval
? self.comboKeyTimeIntervalMs :
( isIntervalCmd ? self.getMilliSecIntervalFrom(intervalString: cmdString) : 0)
if hasUnifiedTriggerInterval || !isIntervalCmd {
self.handleButtonStringDown(cmdString)
}
if cmdString != comboStrings.last {
try? await Task.sleep(nanoseconds: UInt64(triggerDelay)*1_000_000)
}
}
}
} else {
DispatchQueue.global(qos: .userInteractive).async {
for comboString in comboStrings {
self.handleButtonStringDown(comboString)
if comboString != comboStrings.last {
usleep(hasUnifiedTriggerInterval ? self.comboKeyTimeIntervalMs*1000 : 0) // delay xxx ms
}
}
}
}
}
private func handleButtonStringUp(_ buttonString: String) {
if buttonString.contains(".") {return}
DispatchQueue.global(qos: .userInteractive).async {
if CommandManager.oscButtonMappings.keys.contains(buttonString) {
self.sendOscButtonUpEvent(oscString: buttonString)
}
if CommandManager.keyboardButtonMappings.keys.contains(buttonString) {
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings[buttonString]!,Int8(KEY_ACTION_UP), 0)
}
if CommandManager.mouseButtonMappings.keys.contains(buttonString) {
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), Int32(CommandManager.mouseButtonMappings[buttonString]!))
}
}
}
private func sendComboButtonsUpEvent(comboStrings: [String]) {
let hasUnifiedTriggerInterval = self.hasUnifiedTriggerInterval
if hasUnifiedTriggerInterval {
self.comboKeyTimeIntervalMs = self.getMilliSecIntervalFrom(intervalString: comboStrings.last!)
}
if #available(iOS 13.0, *) {
Task {
for cmdString in comboStrings {
let isIntervalCmd = cmdString.contains("MS")
let triggerDelay = hasUnifiedTriggerInterval
? self.comboKeyTimeIntervalMs :
( isIntervalCmd ? self.getMilliSecIntervalFrom(intervalString: cmdString) : 0)
if hasUnifiedTriggerInterval || !isIntervalCmd {
self.handleButtonStringUp(cmdString)
}
if cmdString != comboStrings.last {
try? await Task.sleep(nanoseconds: UInt64(triggerDelay)*1_000_000)
}
}
}
} else {
DispatchQueue.global(qos: .userInteractive).async {
for comboString in comboStrings {
self.handleButtonStringUp(comboString)
if comboString != comboStrings.last {
self.handleButtonStringUp(comboString)
usleep(self.comboKeyTimeIntervalMs*1000) // delay xxx ms
}
}
}
}
}
//==============================================================================
// Touch event handling
private func getAllSpawnedTouchesCount(with event: UIEvent?)->Int{
return UITouchUtil.touches(in: self, from: event).count
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.touchBegan = true
self.directionPadTouchBegan = true
self.firstTouchMoved = false
self.tickFlag = 0
super.touchesBegan(touches, with: event)
self.isMultipleTouchEnabled = self.widgetType == WidgetTypeEnum.button
|| CommandManager.mousePadWithButtonActions.contains(self.touchPadString)
|| self.touchPadString == "MAGNIFIER"
if !OnScreenWidgetView.editMode && self.touchPadString == "TRACKBALL" {
stopTrackballMomentum()
}
guard let touch = touches.first else {return}
// get touchBeganLocation
if touches.count == 1 { // to make sure touchBegan location captured properly, don't use event.alltouches.count here
let currentTime = CACurrentMediaTime()
touchTapTimeInterval = currentTime - touchTapTimeStamp
touchTapTimeStamp = currentTime
quickDoubleTapDetected = touchTapTimeInterval < QUICK_TAP_TIME_INTERVAL
if quickDoubleTapDetected, handleQuickDoubleTapAction() {
quickDoubleTapDetected = false
return
}
if quickDoubleTapDetected, self.isFolder, self.buttonMode == .slideAndHold {
self.temporarilyMovable = true
}
if OnScreenWidgetView.editMode {
self.touchBeganLocation = touch.location(in: superview)
self.highlightBorder(highlighted: true)
}
else {
if widgetType == WidgetTypeEnum.button, self.buttonMode == .movable {self.touchBeganLocation = touch.location(in: superview)}
else {self.touchBeganLocation = touch.location(in: self)}
}
self.latestTouchLocation = touchBeganLocation
}
allSpawnedTouchesCount = UITouchUtil.touches(in: self, from: event).count // this will counts all valid touches within the self widgetView, and excludes touches in other widgetViews
if allSpawnedTouchesCount == 2 {
self.twoTouchesDetected = true
}
if !OnScreenWidgetView.editMode {
if self.widgetType == WidgetTypeEnum.touchPad && touches.count == 1{ // don't use event?.allTouches?.count here, it will counts all touches including the ones captured by other UIViews
switch self.touchPadString {
case "LSWHEEL","RSWHEEL":
GenericUtils.handleStickWheelTip(in: self.parentViewController)
self.getVector(touch: touch)
self.handleStickWheelMove(touch: touch)
if quickDoubleTapDetected {
self.showl3r3Indicator()
self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)}
case "LSVPAD":
self.clearLeftStickTouchPadFlag()
self.inertialScroller.timer?.pause()
if quickDoubleTapDetected {
self.showl3r3Indicator()
self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)}
case "RSVPAD":
GenericUtils.handleVelocityBasedTouchpadTip(in: self.parentViewController)
self.clearRightStickTouchPadFlag()
self.inertialScroller.timer?.pause()
if quickDoubleTapDetected {
self.showl3r3Indicator()
self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)}
case "DS4TOUCH":
if quickDoubleTapDetected {
self.showl3r3Indicator()
self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)}
case "DPAD", "WASDPAD", "ARROWPAD":
if allSpawnedTouchesCount == 1 {
// showLrudBall(at: touchBeganLocation)
CATransaction.begin()
CATransaction.setDisableActions(true)
self.stickWheelAxis.position = touchPointAnchored ? touchBeganLocation : CGPoint(x: self.bounds.midX, y: self.bounds.midY)
self.stickWheelAxis.isHidden = false
CATransaction.commit()
self.getVector(touch: touch)
self.handleLrudTouchMove()
}
if quickDoubleTapDetected && self.touchPadString == "DPAD" {
self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)
DispatchQueue.global(qos: .userInteractive).async {
usleep(100000)
self.sendComboButtonsUpEvent(comboStrings: self.comboButtonStrings)
}
}
case "LTPAD", "RTPAD","MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL":
if quickDoubleTapDetected && !self.comboButtonStrings.isEmpty {
self.showl3r3Indicator()
self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)
}
case "MAGNIFIER":
GenericUtils.handleMagnifierTip(in: self.parentViewController)
if quickDoubleTapDetected && allSpawnedTouchesCount == 1 {
OnScreenWidgetView.profileChangedDuringStreaming = true
self.functionalWidgetDelegate?.resetMagnifierStreamView(animated: self.animatesTransition)
}
default:
break
}
if self.widgetType == WidgetTypeEnum.touchPad && CommandManager.mousePadWithButtonActions.contains(self.touchPadString) && allSpawnedTouchesCount == 1 && !twoTouchesDetected {
self.handleMousePadButtonActionDown(touch: touch)
}
}
if touches.count == 1 {
switch self.touchPadString {
case "LSPAD":
self.hasTrackPoint = touchPointAnchored || widgetType == .button
self.getVector(touch: touch)
self.weightedOffsetX = self.offsetX * self.sensitivityFactorX
self.weightedOffsetY = self.offsetY * self.sensitivityFactorY
self.inertialScroller.timer?.pause()
if touchPointAnchored {self.clearLeftStickTouchPadFlag()}
else {self.sendLeftStickTouchPadEvent(weightedTouchX: weightedOffsetX, weightedTouchY: weightedOffsetY, circulate: true)}
if widgetType == .touchPad {
self.showStickIndicator()
if !touchPointAnchored {
self.stickThumb.fillColor = UIColor(white: 1, alpha: 0.6).cgColor
self.stickThumb.strokeColor = UIColor(white: 0, alpha: 0.3).cgColor
self.updateStickIndicator()
}
if quickDoubleTapDetected {
self.showl3r3Indicator()
self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)}
}
case "RSPAD":
self.hasTrackPoint = touchPointAnchored || widgetType == .button
self.getVector(touch: touch)
self.weightedOffsetX = self.offsetX * self.sensitivityFactorX
self.weightedOffsetY = self.offsetY * self.sensitivityFactorY
self.inertialScroller.timer?.pause()
if touchPointAnchored {self.clearRightStickTouchPadFlag()}
else {self.sendRightStickTouchPadEvent(weightedTouchX: weightedOffsetX, weightedTouchY: weightedOffsetY, circulate: true)}
if widgetType == .touchPad {
self.showStickIndicator()
if !touchPointAnchored {
self.stickThumb.fillColor = UIColor(white: 1, alpha: 0.6).cgColor
self.stickThumb.strokeColor = UIColor(white: 0, alpha: 0.3).cgColor
self.updateStickIndicator()
}
if quickDoubleTapDetected {
self.showl3r3Indicator()
self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)}
}
default:
break
}
}
if self.widgetType == WidgetTypeEnum.touchPad && self.touchPadString == "DS4TOUCH" {
self.handleControllerTouchesDown(touches: touches)
}
// this will also deal with button events
if self.widgetType == WidgetTypeEnum.button && !self.comboButtonStrings.isEmpty {
switch self.buttonMode {
case .tapToToggle:
if(self.tapToToggleFlag) {self.handleTapDownOrSlidein()}
else {self.handleFingerUpOrSlideout()}
self.tapToToggleFlag = !self.tapToToggleFlag
case .slideToToggle where !self.isFolder:
self.handleButtonSliding(touches: touches)
case .slideAndHold where !self.isFolder:
self.handleButtonSliding(touches: touches)
default:
self.handleTapDownOrSlidein()
setLock.lock()
self.capturedTouches.union(touches)
setLock.unlock()
}
}
if self.widgetType == WidgetTypeEnum.button && (self.buttonMode == .movable || self.temporarilyMovable) {
movableButtonReleased = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
OnScreenWidgetView.updateStreamViewGuidelines(for: self)
}
}
}
// here is in edit mode:
else{
self.handleButtonDown()
NotificationCenter.default.post(name: Notification.Name("OnScreenWidgetViewSelected"),object: self) // inform layout tool controller to fetch button size factors. self will be passed as the object of the notification
}
if self.hasTrackPoint, OnScreenWidgetView.trackPointEnabled {
CATransaction.begin()
CATransaction.setDisableActions(true)
let trackPointGap = allSpawnedTouchesCount - trackPointPool.count
if trackPointGap > 0 {
for _ in 0..<trackPointGap {
trackPointPool.insert(GraphicUtils.makeTouchTrackpoint(in: self))
}
}
var trackPointPoolIterator = trackPointPool.makeIterator()
for touch in touches {
guard var trackPoint = trackPointPoolIterator.next() else {continue}
while trackPointMapping.values.contains(trackPoint) {
guard let newTrackPoint = trackPointPoolIterator.next() else {break}
trackPoint = newTrackPoint
}
trackPointMapping[touch] = trackPoint
trackPoint.isHidden = false
trackPoint.position = touch.location(in: self)
}
CATransaction.commit()
}
}
private func moveByTouch(touch: UITouch){
let currentLocation: CGPoint
if OnScreenWidgetView.editMode {
// self.layer.borderColor = voidlinkPurple
OnScreenWidgetView.capturer = nil
currentLocation = touch.location(in: superview)
forEachWidget { (widget) in
if widget == self {
widget.highlightBorder(highlighted: true, color: standardHighlightColor.cgColor)
}
else if widget.isFolder {
if isLocation(currentLocation, in: widget), !self.sequenceSet.contains(widget.sequence), !widget.isHidden {
// self.layer.borderColor = UIColor.systemYellow.cgColor
widget.highlightBorder(highlighted: true)
OnScreenWidgetView.capturer = widget
return
}
else {widget.highlightBorder(highlighted: false)}
}
}
}
else {
currentLocation = touch.location(in: superview)
}
if !firstTouchMoved, !self.isAdjacentPoints(currentLocation, from: latestTouchLocation, tolerance: 1) {
// First move event
self.latestTouchLocation = currentLocation
self.firstTouchMoved = true
}
let offsetX = currentLocation.x - latestTouchLocation.x;
let offsetY = currentLocation.y - latestTouchLocation.y;
let outOfBoundsX = center.x+offsetX >= (self.superview?.bounds.width)! || center.x+offsetX < 0
let outOfBoundsY = center.y+offsetY >= (self.superview?.bounds.height)! || center.y+offsetY < 0
if firstTouchMoved {
center = CGPoint(x: outOfBoundsX ? center.x : center.x+offsetX, y: outOfBoundsY ? center.y : center.y+offsetY)
storedCenter = center
}
latestTouchLocation = currentLocation
relocatedDuringStreaming = true
// center = currentLocation;
//NSLog("x coord: %f, y coord: %f", self.frame.origin.x, self.frame.origin.y)
if isFolder, bulkMoveEnabled, firstTouchMoved {
self.moveSubWidgetsInBatch(by: CGVector(dx: offsetX, dy: offsetY))
}
if OnScreenWidgetView.editMode {
layoutUpdateDelegate?.updateGuidelinesForOnScreenWidget(self)
}
else {
if self.widgetType == .button {superview?.bringSubviewToFront(self)}
OnScreenWidgetView.updateStreamViewGuidelines(for: self)
}
}
private func handleControllerTouchesDown(touches: Set<UITouch>) {
for touch in touches{
let availablePointerIds = pointerIdPool.subtracting(activePointerIds)
if let pointerId = availablePointerIds.first {
pointerIdDict[ObjectIdentifier(touch)] = pointerId
let coordX = touch.location(in: self).x/self.bounds.width
let coordY = touch.location(in: self).y/self.bounds.height
LiSendControllerTouchEvent(0, UInt8(LI_TOUCH_EVENT_DOWN), pointerId, Float(coordX), Float(coordY), 1)
activePointerIds.insert(pointerId)
}
}
}
private func handleControllerTouchesMove(touches: Set<UITouch>) {
for touch in touches{
if let pointerId = pointerIdDict[ObjectIdentifier(touch)] {
let coordX = touch.location(in: self).x/self.bounds.width
let coordY = touch.location(in: self).y/self.bounds.height
LiSendControllerTouchEvent(0, UInt8(LI_TOUCH_EVENT_MOVE), pointerId, Float(coordX), Float(coordY), 1)
}
}
}
private func handleControllerTouchesUp(touches: Set<UITouch>) {
for touch in touches{
if let pointerId = pointerIdDict[ObjectIdentifier(touch)] {
let coordX = touch.location(in: self).x/self.bounds.width
let coordY = touch.location(in: self).y/self.bounds.height
LiSendControllerTouchEvent(0, UInt8(LI_TOUCH_EVENT_UP), pointerId, Float(coordX), Float(coordY), 1)
activePointerIds.remove(pointerId)
pointerIdDict.removeValue(forKey: ObjectIdentifier(touch))
}
}
}
private func handleFingerUpAfterSliding(touches: Set<UITouch>, event: UIEvent? = nil) {
func processWidget(_ widget: OnScreenWidgetView , with touch: UITouch) {
setLock.lock()
let captured = widget.capturedTouches.contains(touch)
setLock.unlock()
if !captured || widget.buttonMode == .regular {return}
// let needReleaseButton = (isLocation(touch.location(in: superview), in: widget) // for slideToToggle & movable+slidable buttons
// || widget.buttonMode == .slideAndHold) // for slideAndHold buttons
widget.handleFingerUpOrSlideout(event: event)
setLock.lock()
widget.capturedTouches.remove(touch)
setLock.unlock()
}
// only called by self
for touch in touches {
var exclusiveFolders: Set<OnScreenWidgetView> = Set()
self.forEachWidget(){ widget in // self included here.
guard widget.revealMode != .exclusive || !isLocation(touch.location(in: superview), in: widget) else {
exclusiveFolders.insert(widget)
return
}
processWidget(widget, with: touch)
}
for folder in exclusiveFolders {
processWidget(folder, with: touch)
}
}
}
private func forEachWidget(_ action: (OnScreenWidgetView) -> Void) {
// 遍历 superview 的 subviews,如果 superview 为 nil,则遍历空数组
for subview in self.superview?.subviews ?? [] {
if let widget = subview as? OnScreenWidgetView {
action(widget)
}
}
}
private func isLocation(_ location:CGPoint, in widget:OnScreenWidgetView) -> Bool{
let locationInWidget = widget.convert(location, from: self.superview)
return widget.bounds.contains(locationInWidget)
}
private func handleButtonSliding(touches: Set<UITouch>) {
for touch in touches {
let locationInSuperView = touch.location(in: self.superview)
self.forEachWidget{ widget in
if widget.widgetType != WidgetTypeEnum.button {return}
let isSlidableButton = (widget.buttonMode == .slideToToggle
|| widget.buttonMode == .slideAndHold
|| (widget.buttonMode == .movable && widget != self)
)
if isLocation(locationInSuperView, in: widget){
setLock.lock()
let captured = widget.capturedTouches.contains(touch)
setLock.unlock()
if captured || !isSlidableButton
{return}
setLock.lock()
widget.capturedTouches.add(touch)
setLock.unlock()
widget.handleTapDownOrSlidein()
// print("UIButton: \(widget.buttonLabel) in, \(widget.touchPadString), \(CACurrentMediaTime())")
}
else{
setLock.lock()
let captured = widget.capturedTouches.contains(touch)
setLock.unlock()
if !captured || !isSlidableButton {return}
// print("UIButton: \(widget.buttonLabel) out test, \(widget.touchPadString), \(CACurrentMediaTime())")
if(widget.buttonMode == .slideToToggle || widget.buttonMode == .movable){
widget.handleFingerUpOrSlideout(leaveNonSkillButtonAlone: widget.isFunctionalButton || widget.containsShortcutAction)
setLock.lock()
widget.capturedTouches.remove(touch)
setLock.unlock()
}
if(widget.buttonMode == .slideAndHold){
// do nothing here
}
}
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
if !OnScreenWidgetView.editMode {
if !self.touchPadString.isEmpty{
handleTouchPadMoveEvent(touches, with: event)
}
if self.widgetType == WidgetTypeEnum.button {
if self.buttonMode == .slideToToggle || self.buttonMode == .slideAndHold {self.handleButtonSliding(touches: touches)}
}
if (self.buttonMode == .movable || self.temporarilyMovable) && self.moveableButtonLongPressed() {
if let touch = touches.first {
self.moveByTouch(touch: touch)
}
}
}
// Move the widgetView based on touch movement in relocation mode
if OnScreenWidgetView.editMode {
if let touch = touches.first {
self.moveByTouch(touch: touch)
}
self.anchorBall.removeFromSuperlayer()
self.stickAnchorLayer.removeFromSuperlayer()
}
if self.hasTrackPoint, OnScreenWidgetView.trackPointEnabled {
CATransaction.begin()
CATransaction.setDisableActions(true)
for touch in touches {
if let trackPoint = trackPointMapping[touch] {
trackPoint.position = touch.location(in: self)
}
}
CATransaction.commit()
}
}
func isAdjacentPoints(_ currentPoint: CGPoint, from originalPoint: CGPoint, tolerance: CGFloat) -> Bool {
let distance = hypot(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y)
let threshold = hypot(tolerance, tolerance)
return distance <= threshold
}
private func getVector (touch: UITouch) {
let currentTouchLocation: CGPoint = (touch.location(in: self))
if !self.mousePointerMoved, !self.isAdjacentPoints(currentTouchLocation, from: latestTouchLocation, tolerance: 2.0){
self.mousePointerMoved = true
}
if !firstTouchMoved, !self.isAdjacentPoints(currentTouchLocation, from: touchBeganLocation, tolerance: slideThreshold) {
// First move event
self.latestTouchLocation = currentTouchLocation
self.firstTouchMoved = true
}
self.deltaX = currentTouchLocation.x - self.latestTouchLocation.x
self.deltaY = currentTouchLocation.y - self.latestTouchLocation.y
if self.hasInertia && firstTouchMoved {
self.inertialScroller.vector = UITouchUtil.vector(of: touch, in: self)
}
// touchCenteredOffset = (!self.isStickWheel
// && !(self.isDirectionPad && sensitivityFactorX==0 && sensitivityFactorY==0))
self.offsetX = (!touchPointAnchored && widgetType == .touchPad) ? currentTouchLocation.x - self.bounds.midX : currentTouchLocation.x - self.touchBeganLocation.x
self.offsetY = (!touchPointAnchored && widgetType == .touchPad) ? currentTouchLocation.y - self.bounds.midY : currentTouchLocation.y - self.touchBeganLocation.y
/*
if hasDisplacementBasedStickPads {
let circulatedOffset = ControllerUtil.circulated(offsetVector: CGVector(dx: offSetX, dy: offSetY))
self.offSetX = circulatedOffset.dx
self.offSetY = circulatedOffset.dy
} */
}
private func updateTouchLocation(touch: UITouch){
let currentTouchLocation: CGPoint = (touch.location(in: self))
if weightedDeltaX != 0 || weightedDeltaY != 0 {
self.latestTouchLocation = currentTouchLocation
}
}
private func handleTouchPadMoveEvent (_ touches: Set<UITouch>, with event: UIEvent?){
guard let touch = touches.first else { return }
if touches.count == 1{ // don't use event.alltouches.count here, it will counts all touches
self.getVector(touch: touch)
switch self.touchPadString{
case "MOUSEPAD":
DispatchQueue.global(qos: .userInteractive).async {
self.weightedDeltaX = Int(self.deltaX * 1.7 * self.sensitivityFactorX)
self.weightedDeltaY = Int(self.deltaY * 1.7 * self.sensitivityFactorY)
if self.firstTouchMoved {LiSendMouseMoveEvent(Int16(self.weightedDeltaX), Int16(self.weightedDeltaY))}
self.updateTouchLocation(touch: touch)
}
break
case "TRACKBALL":
DispatchQueue.global(qos: .userInteractive).async {
self.weightedDeltaX = Int(self.deltaX * 1.7 * self.sensitivityFactorX)
self.weightedDeltaY = Int(self.deltaY * 1.7 * self.sensitivityFactorY)
if self.firstTouchMoved {
LiSendMouseMoveEvent(Int16(self.weightedDeltaX), Int16(self.weightedDeltaY))
self.trackballVelocity = CGPoint(x: self.weightedDeltaX, y: self.weightedDeltaY)
self.stopTrackballMomentum()
}
self.updateTouchLocation(touch: touch)
}
break
case "ABSMOUSE":
self.weightedDeltaX = Int(self.deltaX * 1.7 * self.sensitivityFactorX)
self.weightedDeltaY = Int(self.deltaY * 1.7 * self.sensitivityFactorY)
if self.firstTouchMoved {
if !self.absMousePaused {
let reachedEdgeMask = LiSendMouseMoveAsMousePositionEvent(Int16(truncatingIfNeeded: self.weightedDeltaX), Int16(truncatingIfNeeded: self.weightedDeltaY), Int16(self.superViewWidth), Int16(self.superViewHeight))
if reachedEdgeMask>0 {
self.handleMousePadButtonActionUp()
self.absMousePaused = true
DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) {
LiSendMousePositionEvent(Int16(self.superViewWidth)/2, Int16(self.superViewHeight)/2, Int16(self.superViewWidth), Int16(self.superViewHeight))
self.handleMousePadButtonActionDown()
self.absMousePaused = false
}
}
}
}
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":
DispatchQueue.global(qos: .userInteractive).async {
self.weightedDeltaX = 1
self.weightedOffsetX = self.offsetX * self.sensitivityFactorX
self.weightedOffsetY = self.offsetY * self.sensitivityFactorY
self.sendLeftStickTouchPadEvent(weightedTouchX: self.weightedOffsetX, weightedTouchY: self.weightedOffsetY, circulate: true)
}
if widgetType == WidgetTypeEnum.touchPad {updateStickIndicator()}
self.updateTouchLocation(touch: touch)
case "RSPAD":
DispatchQueue.global(qos: .userInteractive).async {
self.weightedDeltaX = 1
self.weightedOffsetX = self.offsetX * self.sensitivityFactorX
self.weightedOffsetY = self.offsetY * self.sensitivityFactorY
self.sendRightStickTouchPadEvent(weightedTouchX: self.weightedOffsetX, weightedTouchY: self.weightedOffsetY, circulate: true)
}
if widgetType == WidgetTypeEnum.touchPad {updateStickIndicator()}
self.updateTouchLocation(touch: touch)
case "LSVPAD":
DispatchQueue.global(qos: .userInteractive).async {
self.weightedDeltaX = Int(self.deltaX*self.VectorStickFactor*self.sensitivityFactorX)
self.weightedDeltaY = Int(self.deltaY*self.VectorStickFactor*self.sensitivityFactorY)
if self.firstTouchMoved {
self.sendLeftStickTouchPadEvent(weightedTouchX: CGFloat(self.weightedDeltaX), weightedTouchY: CGFloat(self.weightedDeltaY))
}
self.updateTouchLocation(touch: touch)
}
case "RSVPAD":
DispatchQueue.global(qos: .userInteractive).async {
self.weightedDeltaX = Int(self.deltaX*self.VectorStickFactor*self.sensitivityFactorX)
self.weightedDeltaY = Int(self.deltaY*self.VectorStickFactor*self.sensitivityFactorY)
if self.firstTouchMoved {
self.sendRightStickTouchPadEvent(weightedTouchX: CGFloat(self.weightedDeltaX), weightedTouchY: CGFloat(self.weightedDeltaY))
}
self.updateTouchLocation(touch: touch)
}
case "LTPAD":
DispatchQueue.global(qos: .userInteractive).async {
self.weightedDeltaY = 1
self.sendLeftTriggerTouchPadEvent(inputY: -self.offsetY*4.5*self.sensitivityFactorY)
self.updateTouchLocation(touch: touch)
}
case "RTPAD":
DispatchQueue.global(qos: .userInteractive).async {
self.weightedDeltaY = 1
self.sendRightTriggerTouchPadEvent(inputY: -self.offsetY*4.5*self.sensitivityFactorY)
self.updateTouchLocation(touch: touch)
}
case "DPAD", "WASDPAD", "ARROWPAD":
self.weightedDeltaX = 1
handleLrudTouchMove()
self.updateTouchLocation(touch: touch)
case "MOUSEWHEEL","WHEEL":
self.weightedDeltaY = Int(self.deltaY*7.5*self.sensitivityFactorY)
if firstTouchMoved {LiSendHighResScrollEvent(Int16(self.weightedDeltaY))}
self.updateTouchLocation(touch: touch)
case "DISCRETEWHEEL", "DSWHEEL":
let currentLocation = touch.location(in: self)
tickFlag = (tickFlag+1)%UInt8(CGFloat(tickCycle)/abs(sensitivityFactorY))
var delta = self.deltaY
self.weightedDeltaY = 1
if delta==0 {self.weightedDeltaY = 0}
if delta==0 {delta=self.touchBeganLocation.y-currentLocation.y}
delta = delta * CGFloat(copysign(1.0, sensitivityFactorY))
if tickFlag == 1, delta != 0 {LiSendScrollEvent(delta > 0 ? -3 : 3)}
// self.latestTouchLocation = currentLocation
self.updateTouchLocation(touch: touch)
default:
break
}
}
if self.widgetType == WidgetTypeEnum.touchPad && self.touchPadString == "DS4TOUCH" {
self.handleControllerTouchesMove(touches: touches)
}
if self.widgetType == WidgetTypeEnum.touchPad && self.touchPadString == "MAGNIFIER" {
OnScreenWidgetView.profileChangedDuringStreaming = true
self.handleMagnifierPadMove(touches: touches, event: event)
}
}
private func handleMagnifierPadMove(touches: Set<UITouch>, event: UIEvent?){
guard let event = event else { return }
let currentTouches = Array(UITouchUtil.touches(in: self, from: event))
self.functionalWidgetDelegate?.setMagnifierViewportInteractionEnabled(true)
switch currentTouches.count {
case 1:
guard let touch = currentTouches.first else { return }
var vector = UITouchUtil.vector(of: touch, in: self)
let moveHorizontally = abs(vector.dx) > abs(vector.dy)
vector = CGVector(dx: moveHorizontally ? vector.dx : 0, dy: moveHorizontally ? 0 : vector.dy)
let translation = CGVector(
dx: vector.dx * self.sensitivityFactorX,
dy: vector.dy * self.sensitivityFactorY
)
self.functionalWidgetDelegate?.magnifierMoveStreamView(translation: translation)
case 2:
let touch1 = currentTouches[0]
let touch2 = currentTouches[1]
let translationVector = UITouchUtil.midPointVector(between: touch1, and: touch2, in: self)
let pinchDelta = (UITouchUtil.distance(between: touch1, and: touch2, in: self)
- UITouchUtil.previousDistance(between: touch1, and: touch2, in: self))
let translation = CGVector(
dx: translationVector.dx * self.sensitivityFactorX,
dy: translationVector.dy * self.sensitivityFactorY
)
self.functionalWidgetDelegate?.magnifierMoveStreamView(
translation: translation,
pinchDelta: pinchDelta * self.sensitivityFactorX
)
default:
break
}
}
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
}
}
}
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
}
}
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
}
}
}
private func handleMotionControlButtonDown(){
GenericUtils.handleGyroButtonTip(in: self.parentViewController)
switch self.motionControlButtonString {
case "GYRO":
self.motionHandler?.startGyroByOnScreenButton(self, yawFactor: yawFactor, pitchFactor: pitchFactor, rollFactor: rollFactor)
if !OnScreenWidgetView.gamepadArrivalReported {OnScreenWidgetView.gamepadArrivalReported = oscProfile.mapGyroTo == .mapGyroToControllerStick}
case "GYROPAUSE":
self.motionHandler?.stopGyroUpdate(interruptNoneGyroInput:false)
if !OnScreenWidgetView.gamepadArrivalReported {OnScreenWidgetView.gamepadArrivalReported = oscProfile.mapGyroTo == .mapGyroToControllerStick}
break
case "ACCEL":
break
case "MOTION":
break
default:
break
}
}
private func handleMotionControlButtonUp(){
switch self.motionControlButtonString {
case "GYRO":
if let gyroStarter = motionHandler?.gyroStarter as? OnScreenWidgetView, self === gyroStarter {
self.forEachWidget{ widget in
if widget.motionControlButtonString != "GYRO" || widget === self {return}
if(widget.buttonMode == .tapToToggle && widget.logicallyDown) {
widget.buttonUpVisualEffect()
widget.tapToToggleFlag = !widget.tapToToggleFlag
}
}
self.motionHandler?.stopGyroUpdate(interruptNoneGyroInput: false, resetLeftStick: true)
self.motionHandler?.gyroStarter = nil
}
else {
if self.motionHandler?.gyroStarter != nil {
if let gyroStarter = motionHandler?.gyroStarter as? OnScreenWidgetView {
self.motionHandler?.startGyroByOnScreenButton(self, yawFactor: gyroStarter.yawFactor, pitchFactor: gyroStarter.pitchFactor, rollFactor: gyroStarter.rollFactor)
}
}
}
case "GYROPAUSE":
if self.motionHandler?.gyroStarter != nil {
self.motionHandler?.startGyroByOnScreenButton(self, yawFactor: motionHandler?.widgetYawFactor ?? 0, pitchFactor: motionHandler?.widgetPitchFactor ?? 0, rollFactor: motionHandler?.widgetRollFactor ?? 0)
}
case "ACCEL":
break
case "MOTION":
break
default:
break
}
}
private func handleFunctionalButtonDown(){
if autoDockIsDocked {return}
switch self.functionalButtonString {
case "FOLDER":
if self.buttonMode != .slideAndHold {break}
GenericUtils.handleSlideAndHoldFolderButtonTip(in: self.parentViewController)
self.folded = false
OnScreenWidgetView.set(folded: false, for: self)
case "ABSTCHDRAG":
let mouseButton = CommandManager.mouseButtonMappings[Set(self.comboButtonStrings).intersection(CommandManager.mouseButtonMappings.keys).first ?? "MLEFT"] ?? BUTTON_LEFT
print("mouseButton \(mouseButton)");
self.functionalWidgetDelegate?.alterAbsTouchDragWith(mouseButton:mouseButton)
case "PENCILHOVER":
if !self.isPencilProEnabled() {break}
self.functionalWidgetDelegate?.enablePencilHover()
case "NOSINGLETOUCH":
if !self.isPencilProEnabled() {break}
self.functionalWidgetDelegate?.setAllowSingleTouchEnabled(false)
default:
break
}
}
private var movableButtonReleased:Bool = true
private func moveableButtonLongPressed() -> Bool{
return !movableButtonReleased && CACurrentMediaTime() - self.touchTapTimeStamp > 0.3
}
private var singleTouchEnabled:Bool = true
private func handleFunctionalButtonUp(event: UIEvent? = nil){
// print("handleFunctionalButtonUp \(self.widgetLabel), event Empty: \(String(describing: event)), \(CACurrentMediaTime())")
if autoDockIsDocked {return}
if buttonMode == .movable {
if moveableButtonLongPressed() && !UITouchUtil.touches(in: self, from: event).isEmpty {return}
switch self.functionalButtonString {
// case "FOLDER":
// self.folded = !self.folded
// OnScreenWidgetView.set(folded: self.folded, for: self)
case "NOSINGLETOUCH":
if !self.isPencilProEnabled() {break}
singleTouchEnabled = !singleTouchEnabled
self.functionalWidgetDelegate?.setAllowSingleTouchEnabled(singleTouchEnabled)
return
default:
break
}
}
switch self.functionalButtonString {
case "FOLDER":
self.folded = !self.folded
OnScreenWidgetView.set(folded: self.folded, for: self)
case "SETTINGS":
temporaryDisableFolderButtonAnimation()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
self.functionalWidgetDelegate?.expandSettingsView()
}
case "DISCONNECT":
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
self.functionalWidgetDelegate?.disconnectRemoteSession()
}
case "QUITAPP":
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
self.functionalWidgetDelegate?.disconnectAndQuitApp()
}
case "TOOLBOX":
self.functionalWidgetDelegate?.bringUpToolboxMenu()
case "PIP":
self.functionalWidgetDelegate?.enterPip()
case "WIDGETTOOL":
temporaryDisableFolderButtonAnimation()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
self.functionalWidgetDelegate?.openWidgetLayoutTool()
}
case "PROFILES","WIDGETPROFILES":
temporaryDisableFolderButtonAnimation()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
self.functionalWidgetDelegate?.openWidgetProfileTable(pickProfile: false)
}
case "PICKPROFILE","PICKPRFL":
temporaryDisableFolderButtonAnimation()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
self.functionalWidgetDelegate?.openWidgetProfileTable(pickProfile: true)
}
case "SOFTKEYBOARD":
self.functionalWidgetDelegate?.bringUpSoftKeyboard()
case "ABSTCHDRAG":
self.functionalWidgetDelegate?.alterAbsTouchDragWith(mouseButton:BUTTON_LEFT)
case "PENCILHOVER":
if !self.isPencilProEnabled() {break}
self.functionalWidgetDelegate?.disablePencilHover()
case "NOSINGLETOUCH":
if !self.isPencilProEnabled() {break}
self.functionalWidgetDelegate?.setAllowSingleTouchEnabled(true)
case "BRUSH":
if !self.isPencilProEnabled() {break}
var brushShortcut = self.cmdString.replacingOccurrences(of: "BRUSH+", with: "")
brushShortcut = brushShortcut.replacingOccurrences(of: "BRUSH", with: "")
self.functionalWidgetDelegate?.replaceBrush(shortcut: brushShortcut)
case "ERASER":
if !self.isPencilProEnabled() {break}
var eraserShortcut = self.cmdString.replacingOccurrences(of: "ERASER+", with: "")
eraserShortcut = eraserShortcut.replacingOccurrences(of: "ERASER", with: "")
self.functionalWidgetDelegate?.replaceEraser(shortcut: eraserShortcut)
case "PRESSURECURVE":
if ["com.voidlink.iOS"
, "com.voidlinkextreme.iOS"
, "com.voidlink.tf.debug10.iOS"
].contains(Bundle.main.bundleIdentifier) && GenericUtils.isIPad() {
self.functionalWidgetDelegate?.presentPressureCurveVC()
}
case "DISABLETOUCH":
self.handleTouchDisableButtonUp()
case "GAMEPADOVERLAY":
self.gamepadOverlayButtonUp()
default:
break
}
}
private var touchDisabledFLag:Bool = false
private func handleTouchDisableButtonUp(){
touchDisabledFLag = !touchDisabledFLag
self.setupAtrributedText()
self.functionalWidgetDelegate?.toggleTouch(disabled: touchDisabledFLag)
}
@objc static var gamepadOverlayFLag:Bool = false
private func gamepadOverlayButtonUp(){
self.relocatedDuringStreaming = true
OnScreenWidgetView.gamepadOverlayFLag = !OnScreenWidgetView.gamepadOverlayFLag
self.setupAtrributedText()
if #available(iOS 13.0, *) {
self.functionalWidgetDelegate?.toggleGamepadOverlay(overlayEnabled: OnScreenWidgetView.gamepadOverlayFLag)
}
}
private func temporaryDisableFolderButtonAnimation(){
OnScreenWidgetView.enableFolderAnimation = false
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + 0.17) {
OnScreenWidgetView.enableFolderAnimation = true
}
}
private func clearRightStickTouchPadFlag(){
if !hasInertia {stickOffsetVector = .zero}
let mixRightStickInputToGyro = (oscProfile.mapGyroTo == .mapGyroToControllerStick
&& oscProfile.yawPitchToRightStick)
if !mixRightStickInputToGyro || self.motionHandler?.gyroMixInputStarted() != true {
self.onScreenControls?.clearRightStickTouchPadFlag()
}
self.motionHandler?.mixOnScreenRightStickAndGyroInput(x: 0, y: 0)
}
private func clearLeftStickTouchPadFlag(){
if !hasInertia {stickOffsetVector = .zero}
let mixLeftStickInputToGyro = (oscProfile.mapGyroTo == .mapGyroToControllerStick
&& oscProfile.rollToLeftStick)
if !mixLeftStickInputToGyro || self.motionHandler?.gyroMixInputStarted() != true {
self.onScreenControls?.clearLeftStickTouchPadFlag()
}
self.motionHandler?.mixOnScreenLeftStickAndGyroInput(x: 0, y: 0)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
touchesEnded(touches, with: event)
}
private func inertiaEnabled() -> Bool {
return self.decelerationRateX > 0.50001 || self.decelerationRateY > 0.50001
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.touchBegan = false
super.touchesEnded(touches, with: event)
CATransaction.begin()
CATransaction.setDisableActions(true)
if !(CommandManager.mousePadWithButtonActions.contains(self.touchPadString) && self.mouseButtonAction != .noClick) {quickDoubleTapDetected = false} //do not reset this flag here in mousePad mode with button actions
self.allSpawnedTouchesCount = self.getAllSpawnedTouchesCount(with: event) // this will counts all valid touches within the self widgetView, and excludes touches in other widgetViews
// deal with pure MOUSPAD first
if !OnScreenWidgetView.editMode && self.widgetType == WidgetTypeEnum.touchPad && CommandManager.mousePadWithButtonActions.contains(self.touchPadString) && allSpawnedTouchesCount == 1 && !twoTouchesDetected {
self.handleMousePadButtonActionUp()
}
if !OnScreenWidgetView.editMode && self.widgetType == WidgetTypeEnum.touchPad && CommandManager.mousePadWithButtonActions.contains(self.touchPadString) && twoTouchesDetected && touches.count == allSpawnedTouchesCount { // need to enable multi-touch first
// touches.count == allCapturedTouchesCount means allfingers are lifting
if(self.mouseButtonAction == MouseButtonAction.hovering) {self.sendMouseRightButtonClickEvent()}
twoTouchesDetected = false
firstTouchMoved = false
}
// then other types of pads or buttons with touchPad function
if !OnScreenWidgetView.editMode && !self.touchPadString.isEmpty {
switch self.touchPadString{
case "LSWHEEL":
self.clearLeftStickTouchPadFlag()
self.setHiddenForStickWheelLayer(hidden: true)
case "RSWHEEL":
self.clearRightStickTouchPadFlag()
self.setHiddenForStickWheelLayer(hidden: true)
case "LSPAD":
if self.inertiaEnabled() {
self.getVector(touch: touches.first!)
self.weightedOffsetX = self.offsetX * self.sensitivityFactorX
self.weightedOffsetY = self.offsetY * self.sensitivityFactorY
self.inertialScroller.vector = CGVector(dx: weightedOffsetX, dy: weightedOffsetY)
if self.inertialScroller.handler == nil {
self.inertialScroller.handler = {
self.sendLeftStickTouchPadEvent(weightedTouchX: self.inertialScroller.vector.dx, weightedTouchY: self.inertialScroller.vector.dy, circulate: true)
}
}
self.inertialScroller.timer?.restart()
}
else {self.clearLeftStickTouchPadFlag()}
if widgetType == WidgetTypeEnum.touchPad {self.resetStickIndicator()}
case "RSPAD":
if self.inertiaEnabled() {
self.getVector(touch: touches.first!)
self.weightedOffsetX = self.offsetX * self.sensitivityFactorX
self.weightedOffsetY = self.offsetY * self.sensitivityFactorY
self.inertialScroller.vector = CGVector(dx: weightedOffsetX, dy: weightedOffsetY)
if self.inertialScroller.handler == nil {
self.inertialScroller.handler = {
self.sendRightStickTouchPadEvent(weightedTouchX: self.inertialScroller.vector.dx, weightedTouchY: self.inertialScroller.vector.dy, circulate: true)
}
}
self.inertialScroller.timer?.restart()
}
else {self.clearRightStickTouchPadFlag()}
if widgetType == WidgetTypeEnum.touchPad {self.resetStickIndicator()}
case "LSVPAD":
if self.inertiaEnabled() {
if(!firstTouchMoved) {self.inertialScroller.vector = CGVector(dx: 0, dy: 0)}
if self.inertialScroller.handler == nil {
self.inertialScroller.handler = {
let weightedDeltaX = self.inertialScroller.vector.dx*self.VectorStickFactor*self.sensitivityFactorX
let weightedDeltaY = self.inertialScroller.vector.dy*self.VectorStickFactor*self.sensitivityFactorY
self.sendLeftStickTouchPadEvent(weightedTouchX: weightedDeltaX, weightedTouchY: weightedDeltaY)
}
}
self.inertialScroller.timer?.restart()
}
else {self.clearLeftStickTouchPadFlag()}
case "RSVPAD":
if self.inertiaEnabled() {
if(!firstTouchMoved) {self.inertialScroller.vector = CGVector(dx: 0, dy: 0)}
if self.inertialScroller.handler == nil {
var synthesizedDeltaX:CGFloat = 0
var synthesizedDeltaY:CGFloat = 0
self.inertialScroller.handler = { [self] in
let weightedScrollerVector = CGVector(dx: self.inertialScroller.vector.dx*self.VectorStickFactor*self.sensitivityFactorX, dy: self.inertialScroller.vector.dy*self.VectorStickFactor*self.sensitivityFactorY)
if ((self.motionHandler?.gyroMixInputStarted()) == true) {
let normailizedGyroVector = CGVector(dx: self.stickOffsetToWeightedTouchInput(offset: self.motionHandler?.gyroToStickOffset.dx ?? 0), dy: -self.stickOffsetToWeightedTouchInput(offset: self.motionHandler?.gyroToStickOffset.dy ?? 0))
let xConvergent = normailizedGyroVector.dx.sign != weightedScrollerVector.dx.sign && abs(normailizedGyroVector.dx) <= abs(weightedScrollerVector.dx)
let yConvergent = normailizedGyroVector.dy.sign != weightedScrollerVector.dy.sign && abs(normailizedGyroVector.dy) <= abs(weightedScrollerVector.dy)
synthesizedDeltaX = xConvergent ? normailizedGyroVector.dx + weightedScrollerVector.dx : weightedScrollerVector.dx
synthesizedDeltaY = yConvergent ? normailizedGyroVector.dy + weightedScrollerVector.dy : weightedScrollerVector.dy
self.inertialScroller.vector.dx = synthesizedDeltaX/(self.VectorStickFactor*self.sensitivityFactorX)
self.inertialScroller.vector.dy = synthesizedDeltaY/(self.VectorStickFactor*self.sensitivityFactorY)
}
self.sendRightStickTouchPadEvent(weightedTouchX: weightedScrollerVector.dx, weightedTouchY: weightedScrollerVector.dy)
}
}
self.inertialScroller.timer?.restart()
}
else {self.clearRightStickTouchPadFlag()}
case "TRACKBALL":
if allSpawnedTouchesCount == 1, self.inertiaEnabled() {
if(mousePointerMoved){
self.startTrackballMomentum()
mousePointerMoved = false //reset flag
}
else{
self.stopTrackballMomentum()
}
}
case "LTPAD":
self.onScreenControls?.updateLeftTrigger(0x00)
case "RTPAD":
self.onScreenControls?.updateRightTrigger(0x00)
case "WASDPAD":
self.stickWheelAxis.isHidden = touchPointAnchored
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["W"]!,Int8(KEY_ACTION_UP), 0)
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["A"]!,Int8(KEY_ACTION_UP), 0)
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["S"]!,Int8(KEY_ACTION_UP), 0)
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["D"]!,Int8(KEY_ACTION_UP), 0)
case "ARROWPAD":
self.stickWheelAxis.isHidden = touchPointAnchored
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["LEFT_ARROW"]!,Int8(KEY_ACTION_UP), 0)
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["RIGHT_ARROW"]!,Int8(KEY_ACTION_UP), 0)
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["UP_ARROW"]!,Int8(KEY_ACTION_UP), 0)
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["DOWN_ARROW"]!,Int8(KEY_ACTION_UP), 0)
case "DPAD":
self.stickWheelAxis.isHidden = touchPointAnchored
self.onScreenControls?.releaseControllerButton(LEFT_FLAG)
self.onScreenControls?.releaseControllerButton(RIGHT_FLAG)
self.onScreenControls?.releaseControllerButton(UP_FLAG)
self.onScreenControls?.releaseControllerButton(DOWN_FLAG)
case "DS4TOUCH":
self.handleControllerTouchesUp(touches: touches)
case "MAGNIFIER":
self.functionalWidgetDelegate?.setMagnifierViewportInteractionEnabled(false)
default:
break
}
if self.widgetType == .touchPad {
self.handleButtonUp()
}
}
if !OnScreenWidgetView.isTweakingHighlight {
if CommandManager.stickTouchPads.contains(touchPadString){
self.l3r3Indicator.isHidden = true
}
if CommandManager.directionPads.contains(touchPadString){
self.upIndicator.isHidden = true
self.downIndicator.isHidden = true
self.leftIndicator.isHidden = true
self.rightIndicator.isHidden = true
self.anchorBall.isHidden = true
self.sprintSign.isHidden = true
self.walkSign.isHidden = true
}
if CommandManager.verticalTouchPads.contains(touchPadString){
self.l3r3Indicator.isHidden = true
}
if CommandManager.mousePadWithButtonActions.contains(self.touchPadString) && self.mouseButtonAction == .noClick {
self.l3r3Indicator.isHidden = true
}
}
if !OnScreenWidgetView.editMode && !self.cmdString.contains("+") && !self.comboButtonStrings.isEmpty { // if the command(keystring contains "+", it's a legacy multi-key command
// print("self.comboButtonStrings \(self.comboButtonStrings),\(CACurrentMediaTime())")
if self.buttonMode == .slideToToggle || self.buttonMode == .slideAndHold {
self.handleFingerUpAfterSliding(touches: touches, event: event)
setLock.lock()
self.capturedTouches.minus(touches)
setLock.unlock()
}
}
if !OnScreenWidgetView.editMode && (self.buttonMode != .tapToToggle
&& self.buttonMode != .slideToToggle
&& self.buttonMode != .slideAndHold
&& self == touches.first?.view) {self.handleFingerUpOrSlideout(event:event)}
if !OnScreenWidgetView.editMode && (self.buttonMode == .movable || self.temporarilyMovable) {
movableButtonReleased = true
OnScreenWidgetView.removeStreamViewGuidelines()
for sequence in self.sequenceSet {
let widget = OnScreenWidgetView.mapping[sequence]
if widget?.isHidden == true {widget?.center = self.center}
}
}
if self.hasTrackPoint, OnScreenWidgetView.trackPointEnabled {
CATransaction.begin()
CATransaction.setDisableActions(true)
for touch in touches {
if let trackPoint = trackPointMapping[touch] {
trackPointMapping.removeValue(forKey: touch)
trackPoint.isHidden = true
}
}
CATransaction.commit()
}
self.temporarilyMovable = false
CATransaction.commit()
if OnScreenWidgetView.editMode {
storedCenter = center // Update initial center for next movement
if center != layoutChanges.last {
layoutChanges.append(center)
}
if OnScreenWidgetView.capturer != nil {
if firstTouchMoved {undoRelocation()}
if let capturer = OnScreenWidgetView.capturer {
capturer.highlightBorder(highlighted: false)
OnScreenWidgetView.putWidget(self, into: capturer)
}
}
guard let superview = superview else { return }
// Deactivate existing constraints if necessary
NSLayoutConstraint.deactivate(self.constraints)
// Add new constraints based on the current center position
translatesAutoresizingMaskIntoConstraints = true
// Create new constraints
let newLeadingConstraint = self.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: self.frame.origin.x)
let newTopConstraint = self.topAnchor.constraint(equalTo: superview.topAnchor, constant: self.frame.origin.y)
// Activate the new location constraints
NSLayoutConstraint.activate([newLeadingConstraint, newTopConstraint])
// Trigger layout update
superview.layoutIfNeeded()
self.highlightBorder(highlighted: false)
setupView(); //re-setup widgetView style
if self.widgetType == WidgetTypeEnum.touchPad{
switch self.touchPadString{
case "LSPAD", "RSPAD":
self.showStickIndicator()
self.updateStickIndicator()
default: break
}
}
}
if OnScreenWidgetView.capturer == nil, OnScreenWidgetView.editMode {
if OnScreenWidgetView.isVerticallyAligned {
self.center = CGPoint(x:OnScreenWidgetView.alignedX, y:self.center.y)
OnScreenWidgetView.isVerticallyAligned = false
}
if OnScreenWidgetView.isHorizontallyAligned {
self.center = CGPoint(x:self.center.x, y:OnScreenWidgetView.alignedY)
OnScreenWidgetView.isHorizontallyAligned = false
}
}
OnScreenWidgetView.capturer = nil
}
@objc public func setAutoTapIntervalByText(str: String){
self.autoTapInterval = self.parseTimeToMilliseconds(str)
}
private func parseTimeToMilliseconds(_ input: String) -> Int {
// 最大值:24小时对应的毫秒数
let maxMilliseconds = 24 * 60 * 60 * 1000
// 去除前后空白
let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
let pattern = #"^(\d+)(ms|s|m)?$"#
guard let regex = try? NSRegularExpression(pattern: pattern),
let match = regex.firstMatch(in: trimmed, range: NSRange(trimmed.startIndex..., in: trimmed)) else {
return 0
}
let valueRange = Range(match.range(at: 1), in: trimmed)!
let valueString = String(trimmed[valueRange])
guard let value = Int(valueString) else { return 0 }
var unit = "ms"
if let unitRange = Range(match.range(at: 2), in: trimmed) {
unit = String(trimmed[unitRange])
}
var milliseconds: Int
switch unit {
case "ms":
milliseconds = value
case "s":
milliseconds = value * 1000
case "m":
milliseconds = value * 60 * 1000
default:
return 0
}
// 小于 50 ms 的情况返回 0
if milliseconds < 50 {
return 0
}
// 限制最大值
if milliseconds > maxMilliseconds {
milliseconds = maxMilliseconds
}
return milliseconds
}
@objc public func getAutoTapIntervalStr() -> String {
// 边界限制:小于50ms的当作0,超过24小时的限制为24小时
let maxMilliseconds = 24 * 60 * 60 * 1000
if autoTapInterval < 50 {
return ""
}
let ms = min(autoTapInterval, maxMilliseconds)
if ms % (60 * 1000) == 0 {
// 整分钟
return "\(ms / (60 * 1000))m"
} else if ms % 1000 == 0 {
// 整秒
return "\(ms / 1000)s"
} else {
// 毫秒
return "\(ms)ms"
}
}
static private func setBorder(highlighted:Bool, in color:CGColor, for widget:OnScreenWidgetView){
let highlighted = highlighted || widget == OnScreenWidgetView.capturer
if highlighted {
widget.layer.borderWidth = 3
widget.layer.borderColor = color
}
else {
widget.layer.borderWidth = widget.borderWidth
widget.layer.borderColor = widget.defaultBorderColor
}
}
private func highlightBorder(highlighted:Bool, color:CGColor? = nil) {
if self.isFolder {
OnScreenWidgetView.setBorder(highlighted: highlighted, in: UIColor.systemYellow.cgColor, for: self)
self.forEachWidget{ widget in
if self.sequenceSet.contains(widget.sequence) {
OnScreenWidgetView.setBorder(highlighted: highlighted, in: UIColor.systemYellow.cgColor, for: widget)
}
}
return
}
OnScreenWidgetView.setBorder(highlighted: highlighted, in: color ?? standardHighlightColor.cgColor, for: self)
}
private func isPencilProEnabled() -> Bool {
if !(PencilHandler.shared?.pencilProEnabled ?? false) {
IAPManager.shared.purchase(AddOnProduct.PencilProPack)
return false
}
return true
}
// MARK: - Auto Dock
private static let autoDockExposedEdgeLength: CGFloat = GenericUtils.isIPhone() ? 90 : 90
private static let autoDockExposedThickness: CGFloat = 22
private static let autoDockVerticalInset: CGFloat = 12
@objc var autoDockIdleDuration: TimeInterval = 0
@objc var storedAutoDockIdleDuration: TimeInterval = 0
private static let autoDockInitialAlpha: CGFloat = 0.8
@objc var autoDockSettledAlpha: CGFloat = 0.2
private static let autoDockSettledAlphaDelay: TimeInterval = 2
private static let autoDockReturnDamping: CGFloat = 0.9
private static let autoDockGoDamping: CGFloat = 0.82
private typealias TouchesIMP = @convention(c) (AnyObject, Selector, Set<UITouch>, UIEvent?) -> Void
private static var autoDockOriginalTouchesBeganIMP: TouchesIMP?
private static var autoDockOriginalTouchesMovedIMP: TouchesIMP?
private static var autoDockOriginalTouchesEndedIMP: TouchesIMP?
private static var autoDockOriginalTouchesCancelledIMP: TouchesIMP?
private static let autoDockSwizzlingInstalled: Void = {
let cls: AnyClass = OnScreenWidgetView.self
func swizzle(_ originalSelector: Selector, _ swizzledSelector: Selector) -> TouchesIMP? {
guard let originalMethod = class_getInstanceMethod(cls, originalSelector),
let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector) else { return nil }
let originalIMP = unsafeBitCast(method_getImplementation(originalMethod), to: TouchesIMP.self)
method_exchangeImplementations(originalMethod, swizzledMethod)
return originalIMP
}
autoDockOriginalTouchesBeganIMP = swizzle(#selector(OnScreenWidgetView.touchesBegan(_:with:)), #selector(OnScreenWidgetView.vl_autoDock_touchesBegan(_:with:)))
autoDockOriginalTouchesMovedIMP = swizzle(#selector(OnScreenWidgetView.touchesMoved(_:with:)), #selector(OnScreenWidgetView.vl_autoDock_touchesMoved(_:with:)))
autoDockOriginalTouchesEndedIMP = swizzle(#selector(OnScreenWidgetView.touchesEnded(_:with:)), #selector(OnScreenWidgetView.vl_autoDock_touchesEnded(_:with:)))
autoDockOriginalTouchesCancelledIMP = swizzle(#selector(OnScreenWidgetView.touchesCancelled(_:with:)), #selector(OnScreenWidgetView.vl_autoDock_touchesCancelled(_:with:)))
}()
private var autoDockTimer: Timer?
// private var autoDockStoredCenter: CGPoint?
private var autoDockDockedCenter: CGPoint?
private var autoDockDockedToBottomEdge: Bool = false
private var autoDockDistance: CGFloat = 3
@objc var autoDockIsDocked: Bool = false
@objc var autoDockEnabled: Bool = false
private var autoDockSettledAlphaTimer: Timer?
private var autoDockOriginalBoundsSize: CGSize = .zero
private static func installAutoDockIfNeeded() {
_ = autoDockSwizzlingInstalled
}
@objc private func vl_autoDock_touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard autoDockEnabled else {
OnScreenWidgetView.autoDockOriginalTouchesBeganIMP?(self, #selector(OnScreenWidgetView.touchesBegan(_:with:)), touches, event)
return
}
if autoDockIsDocked {
autoDockRestoreWidget(animated: false)
return
}
autoDockStopCountdown()
OnScreenWidgetView.autoDockOriginalTouchesBeganIMP?(self, #selector(OnScreenWidgetView.touchesBegan(_:with:)), touches, event)
}
@objc private func vl_autoDock_touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard autoDockEnabled else {
OnScreenWidgetView.autoDockOriginalTouchesMovedIMP?(self, #selector(OnScreenWidgetView.touchesMoved(_:with:)), touches, event)
return
}
if autoDockIsDocked {
return
}
autoDockStopCountdown()
OnScreenWidgetView.autoDockOriginalTouchesMovedIMP?(self, #selector(OnScreenWidgetView.touchesMoved(_:with:)), touches, event)
}
@objc private func vl_autoDock_touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
OnScreenWidgetView.autoDockOriginalTouchesEndedIMP?(self, #selector(OnScreenWidgetView.touchesEnded(_:with:)), touches, event)
guard autoDockEnabled else { return }
if OnScreenWidgetView.deferSlideGestureDueToAutoDockRestore {
DispatchQueue.global().asyncAfter(deadline: .now() + 0.02){
OnScreenWidgetView.deferSlideGestureDueToAutoDockRestore = false
}
}
autoDockRestartCountdownIfNeeded()
}
@objc private func vl_autoDock_touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
OnScreenWidgetView.autoDockOriginalTouchesCancelledIMP?(self, #selector(OnScreenWidgetView.touchesCancelled(_:with:)), touches, event)
guard autoDockEnabled else { return }
autoDockRestartCountdownIfNeeded()
}
@objc public func setAutoDock(enabled: Bool) {
autoDockEnabled = enabled
if enabled {
OnScreenWidgetView.installAutoDockIfNeeded()
autoDockRestartCountdownIfNeeded()
}
else {
autoDockStopCountdown()
autoDockStopSettledAlphaTimer()
if autoDockIsDocked {
autoDockRestoreWidget(animated: false)
}
autoDockRestoreOriginalSize()
label.alpha = 1
autoDockRestoreOriginalAlpha()
}
}
@objc public func restartAutoDockCountdown() {
guard autoDockEnabled else { return }
autoDockRestartCountdownIfNeeded()
}
@objc public func cancelAutoDockCountdown() {
autoDockStopCountdown()
}
@objc public func triggerAutoDockNow() {
guard autoDockEnabled else { return }
autoDockStopCountdown()
autoDockWidgetToNearestEdge()
}
@objc public func restoreFromAutoDock(animated: Bool) {
guard autoDockEnabled else { return }
autoDockRestoreWidget(animated: animated)
}
@objc func autoDockStopCountdown() {
autoDockTimer?.invalidate()
autoDockTimer = nil
}
private func autoDockStopSettledAlphaTimer() {
autoDockSettledAlphaTimer?.invalidate()
autoDockSettledAlphaTimer = nil
}
private func autoDockRestoreOriginalAlpha() {
alpha = widgetType == WidgetTypeEnum.touchPad ? 1 : 1
}
private func autoDockApplyTemporarySize() {
// autoDockOriginalBoundsSize = bounds.size
var adjustedFrame = frame
if autoDockDockedToBottomEdge {
adjustedFrame.size.width = OnScreenWidgetView.autoDockExposedEdgeLength
}
else {
adjustedFrame.size.height = OnScreenWidgetView.autoDockExposedEdgeLength
}
frame = adjustedFrame.integral
if shape == "round" {
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
else {
setSquareWidgetCornerRadius()
}
}
private func autoDockRestoreOriginalSize() {
guard autoDockOriginalBoundsSize != .zero else { return }
var adjustedFrame = frame
adjustedFrame.size = autoDockOriginalBoundsSize
frame = adjustedFrame.integral
if let hostView = superview {
if autoDockDockedToBottomEdge {
frame.origin.y = hostView.bounds.height - frame.height
}
else {
frame.origin.x = hostView.bounds.width - frame.width
}
}
if shape == "round" {
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
else {
setSquareWidgetCornerRadius()
}
}
private func autoDockRestartCountdownIfNeeded() {
autoDockStopCountdown()
autoDockStopSettledAlphaTimer()
guard !OnScreenWidgetView.editMode,
autoDockEnabled,
superview != nil,
window != nil,
!isHidden,
// alpha > 0.01,
!autoDockIsDocked else {
return
}
autoDockTimer = Timer.scheduledTimer(withTimeInterval: autoDockIdleDuration, repeats: false) { [weak self] _ in
self?.autoDockWidgetToNearestEdge()
}
}
private func autoDockWidgetToNearestEdge() {
guard autoDockEnabled else {return}
autoDockIdleDuration = storedAutoDockIdleDuration
if !folded {
restartAutoDockCountdown()
return
}
// if hasUnfoldedSubfolders() {return}
self.isUserInteractionEnabled = false
guard let hostView = superview,
!OnScreenWidgetView.editMode,
autoDockEnabled,
!autoDockIsDocked,
!isHidden else {
return
}
hostView.layoutIfNeeded()
layoutIfNeeded()
// autoDockStoredCenter = storedCenter
let rightDistance = hostView.bounds.width - frame.maxX
let bottomDistance = hostView.bounds.height - frame.maxY
autoDockDistance = min(rightDistance, bottomDistance)
autoDockDockedToBottomEdge = bottomDistance < rightDistance
let targetFrame = autoDockTargetFrame(in: hostView.bounds)
autoDockDockedCenter = CGPoint(x: targetFrame.midX, y: targetFrame.midY)
self.autoDockIsDocked = true
UIView.animate(
withDuration: 0.1,
delay: 0,
usingSpringWithDamping: OnScreenWidgetView.autoDockGoDamping,
initialSpringVelocity: 0.35,
options: [.allowUserInteraction, .curveEaseOut]
) {
self.frame = targetFrame
self.transform = CGAffineTransform(scaleX: 0.985, y: 0.985)
if ControllerUtil.activeGCControllers.count > 0, !GenericUtils.iOS26Available {
self.parentViewController?.setNeedsUpdateOfHomeIndicatorAutoHidden()
}
} completion: { _ in
UIView.animate(withDuration: 0, delay: 0, options: [.allowUserInteraction, .curveEaseOut]) {
self.autoDockApplyTemporarySize()
self.transform = .identity
self.label.alpha = 0
self.alpha = OnScreenWidgetView.autoDockInitialAlpha
} completion: { _ in
self.isUserInteractionEnabled = true
self.autoDockStopSettledAlphaTimer()
self.autoDockSettledAlphaTimer = Timer.scheduledTimer(withTimeInterval: OnScreenWidgetView.autoDockSettledAlphaDelay, repeats: false) { [weak self] _ in
guard let self, self.autoDockIsDocked else { return }
UIView.animate(withDuration: 0.17, delay: 0, options: [.allowUserInteraction, .curveEaseOut]) {
self.isUserInteractionEnabled = true
self.alpha = self.autoDockSettledAlpha
}
}
}
}
}
private func autoDockRestoreWidget(animated: Bool) {
OnScreenWidgetView.deferScreenEdgeSysGesturesDueToOnScreenWidgets = true
self.parentViewController?.setNeedsUpdateOfHomeIndicatorAutoHidden()
guard autoDockIsDocked else {
restartAutoDockCountdown()
return
}
let restoreCenter = storedCenter
OnScreenWidgetView.deferSlideGestureDueToAutoDockRestore = true
autoDockStopCountdown()
autoDockStopSettledAlphaTimer()
autoDockRestoreOriginalSize()
label.alpha = 1
autoDockRestoreOriginalAlpha()
let animations = {
self.center = restoreCenter
self.transform = .identity
}
let completion: (Bool) -> Void = { _ in
self.autoDockDockedCenter = nil
self.autoDockIsDocked = false
self.restartAutoDockCountdown()
if OnScreenWidgetView.autoDockRestoreInitByViewResize {
OnScreenWidgetView.autoDockRestoreInitByViewResize = false
OnScreenWidgetView.deferSlideGestureDueToAutoDockRestore = false
}
OnScreenWidgetView.deferScreenEdgeSysGesturesDueToOnScreenWidgets = false
}
if animated {
UIView.animate(
withDuration: autoDockDistance/(175/0.38),
delay: 0,
usingSpringWithDamping: OnScreenWidgetView.autoDockReturnDamping,
initialSpringVelocity: 0.22,
options: [.allowUserInteraction, .curveEaseOut],
animations: animations,
completion: {_ in
completion(true)
}
)
}
else {
animations()
completion(true)
}
}
private func autoDockTargetFrame(in bounds: CGRect) -> CGRect {
var targetFrame = frame
if autoDockDockedToBottomEdge {
targetFrame.origin.x = min(
max(targetFrame.origin.x, OnScreenWidgetView.autoDockVerticalInset),
bounds.width - targetFrame.width - OnScreenWidgetView.autoDockVerticalInset
)
targetFrame.origin.y = bounds.height - OnScreenWidgetView.autoDockExposedThickness
}
else {
targetFrame.origin.y = min(
max(targetFrame.origin.y, OnScreenWidgetView.autoDockVerticalInset),
bounds.height - targetFrame.height - OnScreenWidgetView.autoDockVerticalInset
)
targetFrame.origin.x = bounds.width - OnScreenWidgetView.autoDockExposedThickness
}
return targetFrame.integral
}
/// ================================================================================================================
@objc func getAvailableSequence() -> Int16 {
var sequence:Int16 = 0
self.forEachWidget(){ widget in
sequence = max(sequence, widget.sequence)
}
return sequence+1
}
@objc func moveSubWidgetsInBatch(by vector:CGVector) {
guard isFolder else {return}
for sequence in self.sequenceSet {
guard let widget = OnScreenWidgetView.mapping[sequence] else {return}
widget.storedCenter = CGPoint(x: widget.storedCenter.x+vector.dx, y: widget.storedCenter.y+vector.dy)
if !widget.isHidden {
widget.center = CGPoint(x: widget.center.x+vector.dx, y: widget.center.y+vector.dy)
}
widget.layoutChanges.append(widget.storedCenter)
widget.relocatedDuringStreaming = true
}
}
@objc static var enableFolderAnimation:Bool = true
private static func setCollection(folded:Bool, for folder:OnScreenWidgetView, exception:OnScreenWidgetView? = nil, recursive:Bool = false) {
guard folder.isFolder else {return}
// guard folder.folded != hidden else { return }
folder.folded = folded
folder.setupAtrributedText()
folder.reverseColorPhase(reversed: !folder.folded)
if folded {
for sequence in folder.sequenceSet {
guard let widget = OnScreenWidgetView.mapping[sequence], widget != exception else {continue}
DispatchQueue.main.async {
widget.isUserInteractionEnabled = false
if ((folder.buttonMode != .slideAndHold && widget.widgetType == .touchPad)
|| abs(widget.backgroundAlpha) < 0.1){
widget.highlightBorder(highlighted: OnScreenWidgetView.enableFolderAnimation && folder.animatesTransition, color: OnScreenWidgetView.enableFolderAnimation ? UIColor.systemBlue.cgColor : UIColor.clear.cgColor)
}
let duration = (OnScreenWidgetView.enableFolderAnimation && folder.animatesTransition)
? (folder.buttonMode == .slideAndHold ? 0.05 : widget.standardFoldingInterval)
: 0
UIView.animate(withDuration: duration, animations: {
widget.center = folder.center
},completion: { finished in
widget.isUserInteractionEnabled = !folded
widget.center = folder.folded ? folder.storedCenter : widget.storedCenter
widget.isHidden = folder.folded
if ((folder.buttonMode != .slideAndHold && widget.widgetType == .touchPad)
|| abs(widget.backgroundAlpha) < 0.1){
widget.highlightBorder(highlighted: false)
}
})
}
}
}
else{
for sequence in folder.sequenceSet {
guard let widget = OnScreenWidgetView.mapping[sequence], widget != exception else {continue}
DispatchQueue.main.async {
widget.isUserInteractionEnabled = false
widget.capturedTouches.removeAllObjects()
widget.center = folder.storedCenter
widget.isHidden = false
if ((folder.buttonMode != .slideAndHold && widget.widgetType == .touchPad)
|| abs(widget.backgroundAlpha) < 0.1){
widget.highlightBorder(highlighted: OnScreenWidgetView.enableFolderAnimation && folder.animatesTransition, color: OnScreenWidgetView.enableFolderAnimation ? UIColor.systemBlue.cgColor : UIColor.clear.cgColor)
}
UIView.animate(withDuration: (OnScreenWidgetView.enableFolderAnimation && folder.animatesTransition)
? (folder.buttonMode == .slideAndHold ? 0.05 : widget.standardFoldingInterval)
: 0
, animations: {
widget.center = widget.storedCenter
},completion: { finished in
widget.capturedTouches.removeAllObjects()
widget.isUserInteractionEnabled = !folder.folded
widget.center = folder.folded ? folder.storedCenter : widget.storedCenter
widget.isHidden = folder.folded
if ((folder.buttonMode != .slideAndHold && widget.widgetType == .touchPad)
|| abs(widget.backgroundAlpha) < 0.1){
DispatchQueue.main.asyncAfter(deadline: .now() + (widget.widgetType == .touchPad ? 0.15 : 0)) {
widget.highlightBorder(highlighted: false)
}
}
if widget.hasNonEditableLabel {widget.setupAtrributedText()}
if !OnScreenWidgetView.editMode, widget.widgetType == .touchPad, let deepestButton = OnScreenWidgetView.deepestButton {
widget.superview?.insertSubview(widget, belowSubview: deepestButton)
}
})
}
}
}
if recursive {
for sequence in folder.sequenceSet {
guard let widget = OnScreenWidgetView.mapping[sequence] else {continue}
guard widget.isFolder, widget != exception else {continue}
OnScreenWidgetView.setCollection(folded: folded, for: widget, exception: exception, recursive: true)
}
}
}
private static func setCollection(hidden:Bool, for folder:OnScreenWidgetView) {
for sequence in folder.sequenceSet {
guard let widget = OnScreenWidgetView.mapping[sequence] else {continue}
widget.isHidden = hidden
}
}
@objc static func set(folded:Bool, for folder:OnScreenWidgetView) { // folder综合逻辑
guard folder.isFolder else {return}
setCollection(folded: folded, for: folder)
if !folded, folder.revealMode == .exclusive {
OnScreenWidgetView.unfoldedExclusiveFolderSequence = folder.sequence
if(!isRestoringFolderStates) {OnScreenWidgetView.postExclusiveUnfoldedSequences.removeAll()}
let currentRootFolder = OnScreenWidgetView.getRootFolder(of: folder) ?? folder
var offshootRootFolders:Set<OnScreenWidgetView> = Set()
folder.forEachWidget{ widget in
guard folder != widget else {return}
let rootFolder = getRootFolder(of: widget)
guard let rootFolder = rootFolder else {return}
offshootRootFolders.insert(rootFolder)
}
offshootRootFolders.remove(currentRootFolder)
for _ in offshootRootFolders {
// print("offshootRootFolder \(CACurrentMediaTime()) \(folder.label.text ?? "")")
}
guard !folder.sequenceSet.isEmpty else {return}
for folder in offshootRootFolders {
setCollection(folded: true, for: folder, recursive: true)
}
guard currentRootFolder != folder else {return}
setCollection(folded: true, for:currentRootFolder, exception: folder, recursive: true)
}
if folded, folder.revealMode == .exclusive {
OnScreenWidgetView.unfoldedExclusiveFolderSequence = -1
}
if folder.revealMode == .coexist {
if folded {
OnScreenWidgetView.postExclusiveUnfoldedSequences.remove(folder.sequence)
}
else {
OnScreenWidgetView.postExclusiveUnfoldedSequences.insert(folder.sequence)
for unfoldedExclusiveFolder in OnScreenWidgetView.mapping.values.filter({$0.isFolder
&& !$0.folded
&& $0 != folder
&& $0.revealMode == .exclusive
&& !OnScreenWidgetView.getParentFolders(of: $0).contains(folder)}) {
OnScreenWidgetView.setCollection(folded: true, for: unfoldedExclusiveFolder)
}
}
}
if folder.buttonMode == .slideAndHold, folder.isOnlyRootFolder(), folder.revealMode == .coexist {
for otherUnfoldedFolder in OnScreenWidgetView.mapping.values.filter({$0.isFolder && !$0.folded && $0 != folder}) {
OnScreenWidgetView.setCollection(hidden: !folded, for: otherUnfoldedFolder)
}
}
}
private static func getRootFolder(of widget:OnScreenWidgetView) -> OnScreenWidgetView?{
var widgetRef:OnScreenWidgetView? = widget
var parentFolder:OnScreenWidgetView? = OnScreenWidgetView.mapping[widgetRef?.parentSequence ?? -1]
repeat {
widgetRef = parentFolder
parentFolder = OnScreenWidgetView.mapping[widgetRef?.parentSequence ?? -1]
} while parentFolder != nil
return widgetRef
}
static func getParentFolders(of widget: OnScreenWidgetView) -> Set<OnScreenWidgetView> {
var parents = Set<OnScreenWidgetView>()
var current = widget
while let parent = OnScreenWidgetView.mapping[current.parentSequence] {
parents.insert(parent)
current = parent
}
return parents
}
private func hasUnfoldedSubfolders() -> Bool {
for sequence in self.sequenceSet {
guard let widget = OnScreenWidgetView.mapping[sequence] else {continue}
if !widget.isFolder {continue}
if !widget.folded {return true}
}
return false
}
private func isOnlyRootFolder() -> Bool {
let rootFolders = OnScreenWidgetView.mapping.values.filter({$0.isFolder && $0.parentSequence == -1})
if rootFolders.count != 1 {return false}
return rootFolders.contains(self)
}
@objc static var onlyRootFolderVisible: Bool {
var visibleNonFolderCount:Int = 0
var visibleFolderCount:Int = 0
for widget in OnScreenWidgetView.mapping.values {
if widget.isHidden {continue}
if widget.isFolder {
visibleFolderCount += 1
if visibleFolderCount > 1 {return false}
continue
}
else {
visibleNonFolderCount += 1
if visibleNonFolderCount > 0 {return false}
continue
}
}
return true
}
private static func putWidget(_ widget:OnScreenWidgetView, into folder:OnScreenWidgetView){
guard !OnScreenWidgetView.getParentFolders(of: folder).contains(widget), !folder.isHidden else { return }
let parentFolder = OnScreenWidgetView.mapping[widget.parentSequence]
if parentFolder != nil {
parentFolder?.sequenceSet.remove(widget.sequence)
}
widget.parentSequence = folder.sequence
folder.sequenceSet.insert(widget.sequence)
widget.isHidden = folder.folded
/*
if folder.buttonMode == .slideAndHold, widget.isFunctionalButton {
widget.buttonMode = .slideToToggle
} */
}
@objc static func setFree(widget:OnScreenWidgetView){
let parentFolder = OnScreenWidgetView.mapping[widget.parentSequence]
if parentFolder != nil {
parentFolder?.sequenceSet.remove(widget.sequence)
}
widget.parentSequence = -1
if widget.isFolder {
if widget.folded {
OnScreenWidgetView.set(folded: false, for: widget)
}
for sequence in widget.sequenceSet {
guard let subWidget = OnScreenWidgetView.mapping[sequence] else {continue}
subWidget.parentSequence = -1
}
widget.sequenceSet.removeAll()
}
}
@objc static func restoreFoldedStates(){
isRestoringFolderStates = true
var unfoldedExclusiveFolder: OnScreenWidgetView?
if unfoldedExclusiveFolderSequence != -1 {
unfoldedExclusiveFolder = OnScreenWidgetView.mapping[unfoldedExclusiveFolderSequence]
}
for widget in OnScreenWidgetView.mapping.values {
OnScreenWidgetView.setCollection(folded: widget.folded, for: widget, exception: unfoldedExclusiveFolder)
}
if(unfoldedExclusiveFolder != nil){
OnScreenWidgetView.set(folded: false, for: unfoldedExclusiveFolder!)
if OnScreenWidgetView.mapping[unfoldedExclusiveFolder!.parentSequence]?.buttonMode == .slideAndHold {
unfoldedExclusiveFolder?.isHidden = true
}
}
// print("postExclusiveUnfoldedSequences \(postExclusiveUnfoldedSequences) stamp \(CACurrentMediaTime())")
for sequence in postExclusiveUnfoldedSequences {
guard let folder = OnScreenWidgetView.mapping[sequence] else { continue }
OnScreenWidgetView.set(folded: false, for: folder)
}
isRestoringFolderStates = false
}
@objc static func getDeepestButton() -> OnScreenWidgetView?{
var maxIndex: Int = Int.max
var deepestButton: OnScreenWidgetView?
for widget in OnScreenWidgetView.mapping.values.filter({ $0.widgetType == .button}){
let currentIndex = widget.superview?.subviews.firstIndex(of: widget)
if let currentIndex = currentIndex {
if currentIndex < maxIndex {
maxIndex = currentIndex
deepestButton = widget
}
}
else {continue}
}
OnScreenWidgetView.deepestButton = deepestButton
return deepestButton
}
@objc static func getMaxSequence() -> Int16{
return OnScreenWidgetView.mapping.keys.max() ?? -1
}
@objc static var gamepadArrivalReported: Bool = false
override func didMoveToSuperview() {
super.didMoveToSuperview()
OnScreenWidgetView.installAutoDockIfNeeded()
if superview == nil {
autoDockStopCountdown()
autoDockStopSettledAlphaTimer()
autoDockRestoreOriginalSize()
autoDockIsDocked = false
label.alpha = 1
autoDockRestoreOriginalAlpha()
if self.motionControlButtonString == "GYRO" {
if OnScreenWidgetView.gamepadArrivalReported {self.motionHandler?.stopGyroUpdate(interruptNoneGyroInput: true)}
self.motionHandler?.gyroStarter = nil
}
if self.motionControlButtonString == "ACCEL" {}
if self.motionControlButtonString == "MOTION" {}
if self.widgetType == WidgetTypeEnum.button && !OnScreenWidgetView.editMode {
if OnScreenWidgetView.gamepadArrivalReported {self.sendComboButtonsUpEvent(comboStrings: self.comboButtonStrings)}
self.functionalWidgetDelegate?.alterAbsTouchDragWith(mouseButton:BUTTON_LEFT)
}
buttonDownVisualEffectLayer.removeFromSuperlayer()
for trackPoint in trackPointPool {
trackPoint.removeFromSuperlayer()
}
stickAnchorLayer.removeFromSuperlayer()
anchorBall.removeFromSuperlayer()
l3r3Indicator.removeFromSuperlayer()
upIndicator.removeFromSuperlayer()
downIndicator.removeFromSuperlayer()
leftIndicator.removeFromSuperlayer()
rightIndicator.removeFromSuperlayer()
anchorBall.removeFromSuperlayer()
stickWheelAxis.removeFromSuperlayer()
stickWheelLayer.removeFromSuperlayer()
stickWheelLayerSmall.removeFromSuperlayer()
self.inertialScroller.timer?.clean()
if OnScreenWidgetView.gamepadArrivalReported {
self.clearLeftStickTouchPadFlag()
self.clearRightStickTouchPadFlag()
}
self.autoTapTimer?.clean()
self.onScreenControls = nil
}
else{
self.superViewWidth = (superview?.bounds.size.width)!
self.superViewHeight = (superview?.bounds.size.height)!
autoDockRestartCountdownIfNeeded()
}
}
private static var horizontalGuideline: UIView = {
let v = UIView(
frame: CGRect(
x: 0,
y: 0,
width: UIScreen.main.bounds.width * 2,
height: 2
)
)
v.backgroundColor = .blue
v.isHidden = true
v.layer.shadowColor = UIColor.black.cgColor
v.layer.shadowOffset = .zero
v.layer.shadowOpacity = 0.5
v.layer.shadowRadius = 2
return v
}()
private static var verticalGuideline: UIView = {
let v = UIView(
frame: CGRect(
x: 0,
y: 0,
width: 2,
height: UIScreen.main.bounds.height * 2
)
)
v.backgroundColor = .blue
v.isHidden = true
v.layer.shadowColor = UIColor.black.cgColor
v.layer.shadowOffset = .zero
v.layer.shadowOpacity = 0.5
v.layer.shadowRadius = 2
return v
}()
@objc static var isHorizontallyAligned:Bool = false
@objc static var alignedX:CGFloat = 0
@objc static var isVerticallyAligned:Bool = false
@objc static var alignedY:CGFloat = 0
private static func updateStreamViewGuidelines(for widget:OnScreenWidgetView){
if !widget.moveableButtonLongPressed() {return}
if horizontalGuideline.superview == nil {
widget.addSubview(horizontalGuideline)
widget.addSubview(verticalGuideline)
horizontalGuideline.isHidden = false
verticalGuideline.isHidden = false
}
horizontalGuideline.center = CGPoint(x: widget.bounds.midX, y: widget.bounds.midY)
verticalGuideline.center = CGPoint(x: widget.bounds.midX, y: widget.bounds.midY)
var horizontallyAligned = false
var verticallyAligned = false
widget.forEachWidget{ otherWidget in
guard otherWidget != widget else {return}
if widget.isFolder, widget.bulkMoveEnabled, widget.sequenceSet.contains(otherWidget.sequence) {return}
verticallyAligned = verticallyAligned ? verticallyAligned : widget.center.x > otherWidget.center.x-2 && widget.center.x < otherWidget.center.x+2
OnScreenWidgetView.isVerticallyAligned = verticallyAligned;
if verticallyAligned {OnScreenWidgetView.alignedX = otherWidget.center.x}
horizontallyAligned = horizontallyAligned ? horizontallyAligned : widget.center.y > otherWidget.center.y-2 && widget.center.y < otherWidget.center.y+2
OnScreenWidgetView.isHorizontallyAligned = horizontallyAligned;
if horizontallyAligned {OnScreenWidgetView.alignedY = otherWidget.center.y}
}
horizontalGuideline.backgroundColor = horizontallyAligned ? .yellow : .blue
verticalGuideline.backgroundColor = verticallyAligned ? .yellow : .blue
}
private static func removeStreamViewGuidelines(){
horizontalGuideline.removeFromSuperview()
verticalGuideline.removeFromSuperview()
}
deinit {
print("onScreenWidgetView deinit \(self.widgetLabel))")
}
}
private final class DisplacementStickPadState {
var stickInputScale: CGFloat = 35
let stickAnchorColor: CGColor = UIColor.white.withAlphaComponent(0.75).cgColor
var stickThumbSize: CGFloat = 13
let stickThumbColor: CGColor = UIColor(white: 1, alpha: 0.75).cgColor
let stickThumbMaxOffset: CGFloat = 18
}
extension OnScreenWidgetView {
private static var displacementStickPadStateKey: UInt8 = 0
private var displacementStickPadState: DisplacementStickPadState {
if let state = objc_getAssociatedObject(self, &Self.displacementStickPadStateKey) as? DisplacementStickPadState {
return state
}
let state = DisplacementStickPadState()
objc_setAssociatedObject(
self,
&Self.displacementStickPadStateKey,
state,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
return state
}
private var stickInputScale: CGFloat {
get { displacementStickPadState.stickInputScale }
set { displacementStickPadState.stickInputScale = newValue }
}
private var stickAnchorColor: CGColor {
displacementStickPadState.stickAnchorColor
}
private var stickThumbSize: CGFloat {
get { displacementStickPadState.stickThumbSize }
set { displacementStickPadState.stickThumbSize = newValue }
}
private var stickThumbColor: CGColor {
displacementStickPadState.stickThumbColor
}
private var stickThumbMaxOffset: CGFloat {
displacementStickPadState.stickThumbMaxOffset
}
@objc public func setupL3R3Indicator() {
CATransaction.begin()
CATransaction.setDisableActions(true)
let indicatorFrame = CAShapeLayer()
indicatorFrame.frame = CGRectMake(0, 0, 60 * highlightSizeFactor, 60 * highlightSizeFactor)
indicatorFrame.cornerRadius = indicatorFrame.frame.width/2
l3r3Indicator.borderWidth = 7 * min(highlightSizeFactor, 1.0)
l3r3Indicator.frame = indicatorFrame.bounds.insetBy(dx: -l3r3Indicator.borderWidth, dy: -l3r3Indicator.borderWidth)
l3r3Indicator.borderColor = UIColor.clear.cgColor
l3r3Indicator.cornerRadius = indicatorFrame.cornerRadius + l3r3Indicator.borderWidth
l3r3Indicator.backgroundColor = UIColor.clear.cgColor
l3r3Indicator.fillColor = UIColor.clear.cgColor
l3r3Indicator.path = UIBezierPath(
roundedRect: l3r3Indicator.bounds,
cornerRadius: l3r3Indicator.cornerRadius
).cgPath
l3r3Indicator.borderColor = standardHighlightColor.cgColor
l3r3Indicator.position = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
if !OnScreenWidgetView.isTweakingHighlight { l3r3Indicator.isHidden = true }
if l3r3Indicator.superlayer == nil {
layer.addSublayer(l3r3Indicator)
}
CATransaction.commit()
}
private func showl3r3Indicator() {
if OnScreenWidgetView.buttonVisualFeedbackEnabled {
CATransaction.begin()
CATransaction.setDisableActions(true)
l3r3Indicator.position = CGPoint(x: touchBeganLocation.x, y: touchBeganLocation.y)
l3r3Indicator.isHidden = false
CATransaction.commit()
}
if vibrationOn {
vibrationGenerator.prepare()
vibrationGenerator.impactOccurred()
}
}
private func handleStickThumbReachingBorder() {
CATransaction.begin()
CATransaction.setDisableActions(true)
stickThumb.lineWidth = 1
stickThumb.shadowOffset = .zero
stickThumb.shadowRadius = 4
stickThumb.shadowOpacity = 0.28
CATransaction.commit()
}
private func handleStickThumbLeavingBorder() {
stickThumb.lineWidth = 0
stickThumb.shadowOffset = CGSize(width: 0, height: 2)
stickThumb.shadowRadius = 4
stickThumb.shadowOpacity = 0.24
stickThumb.shadowColor = UIColor.black.cgColor
}
@objc public func showStickIndicator() {
guard self.isDisplacementBasedStickPad else { return }
if self.stickAnchorLayer.superlayer == nil {self.stickAnchorLayer = createStickAnchorLayer()}
if self.stickThumb.superlayer == nil {self.stickThumb = createStickThumb()}
let stickIndicatorLocation: CGPoint
if !OnScreenWidgetView.editMode {
stickIndicatorLocation = CGPoint(x: touchBeganLocation.x, y: touchBeganLocation.y - stickIndicatorOffset)
} else {
stickIndicatorLocation = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
}
getStickThumbReady(at:touchPointAnchored ? stickIndicatorLocation : CGPoint(x: self.bounds.midX, y: self.bounds.midY))
showStickAnchorLayer(at: touchPointAnchored ? stickIndicatorLocation : CGPoint(x: self.bounds.midX, y: self.bounds.midY))
}
private func createStickAnchorLayer() -> CAShapeLayer {
let crossLayer = CAShapeLayer()
let markColor = self.originalBackgroundAlpha>0 ? UIColor.white.withAlphaComponent(abs(self.originalBackgroundAlpha)*0.35).cgColor : UIColor.black.withAlphaComponent(abs(self.originalBackgroundAlpha)*0.35).cgColor
let outlineColor = self.originalBackgroundAlpha>0 ? UIColor.black.withAlphaComponent(0.1).cgColor : UIColor.white.withAlphaComponent(0.1).cgColor
let canvasSize: CGFloat = 56
let center = CGPoint(x: canvasSize / 2, y: canvasSize / 2)
let ringOuterRadius: CGFloat = touchPointAnchored ? 25 : 19/(self.sensitivityFactorX*(stickThumbMaxOffset/stickInputScale))
let ringInnerRadius: CGFloat = ringOuterRadius - (touchPointAnchored ? 2 : 2)
let triangleBaseRadius: CGFloat = touchPointAnchored ? 14 : stickThumbSize+3.7
let triangleApexRadius: CGFloat = touchPointAnchored ? 18 : triangleBaseRadius + (stickThumbSize<20 ? 4 : max(4, stickThumbSize*0.2))
let triangleHalfWidth: CGFloat = touchPointAnchored ? 3.6 : (stickThumbSize<20 ? 4 : max(4, stickThumbSize*0.2))
let strokeLingWdith: CGFloat = 0.5
crossLayer.bounds = CGRect(x: 0, y: 0, width: canvasSize, height: canvasSize)
crossLayer.fillColor = UIColor.clear.cgColor
crossLayer.strokeColor = UIColor.clear.cgColor
let ringLayer = CAShapeLayer()
ringLayer.fillColor = markColor
ringLayer.strokeColor = outlineColor
ringLayer.lineWidth = strokeLingWdith
ringLayer.fillRule = .evenOdd
let ringPath = UIBezierPath(
ovalIn: CGRect(
x: center.x - ringOuterRadius,
y: center.y - ringOuterRadius,
width: ringOuterRadius * 2,
height: ringOuterRadius * 2
)
)
ringPath.append(
UIBezierPath(
ovalIn: CGRect(
x: center.x - ringInnerRadius,
y: center.y - ringInnerRadius,
width: ringInnerRadius * 2,
height: ringInnerRadius * 2
)
).reversing()
)
ringLayer.path = ringPath.cgPath
ringLayer.shadowColor = UIColor.clear.cgColor
ringLayer.shadowOffset = .zero
ringLayer.shadowRadius = 0
ringLayer.shadowOpacity = 0.5
let triangleLayer = CAShapeLayer()
triangleLayer.fillColor = self.originalBackgroundAlpha>0 ? UIColor.white.withAlphaComponent(0.25).cgColor : UIColor.black.withAlphaComponent(0.25).cgColor
triangleLayer.strokeColor = outlineColor
triangleLayer.lineWidth = strokeLingWdith
triangleLayer.lineJoin = .round
let trianglePath = UIBezierPath()
trianglePath.move(to: CGPoint(x: center.x, y: center.y - triangleApexRadius))
trianglePath.addLine(to: CGPoint(x: center.x - triangleHalfWidth, y: center.y - triangleBaseRadius))
trianglePath.addLine(to: CGPoint(x: center.x + triangleHalfWidth, y: center.y - triangleBaseRadius))
trianglePath.close()
trianglePath.move(to: CGPoint(x: center.x, y: center.y + triangleApexRadius))
trianglePath.addLine(to: CGPoint(x: center.x + triangleHalfWidth, y: center.y + triangleBaseRadius))
trianglePath.addLine(to: CGPoint(x: center.x - triangleHalfWidth, y: center.y + triangleBaseRadius))
trianglePath.close()
trianglePath.move(to: CGPoint(x: center.x + triangleApexRadius, y: center.y))
trianglePath.addLine(to: CGPoint(x: center.x + triangleBaseRadius, y: center.y - triangleHalfWidth))
trianglePath.addLine(to: CGPoint(x: center.x + triangleBaseRadius, y: center.y + triangleHalfWidth))
trianglePath.close()
trianglePath.move(to: CGPoint(x: center.x - triangleApexRadius, y: center.y))
trianglePath.addLine(to: CGPoint(x: center.x - triangleBaseRadius, y: center.y + triangleHalfWidth))
trianglePath.addLine(to: CGPoint(x: center.x - triangleBaseRadius, y: center.y - triangleHalfWidth))
trianglePath.close()
triangleLayer.path = trianglePath.cgPath
triangleLayer.shadowColor = UIColor.clear.cgColor
triangleLayer.shadowOffset = .zero
triangleLayer.shadowRadius = 0
triangleLayer.shadowOpacity = 0.5
crossLayer.addSublayer(ringLayer)
crossLayer.addSublayer(triangleLayer)
layer.addSublayer(crossLayer)
crossLayer.isHidden = touchPointAnchored
return crossLayer
}
private func showStickAnchorLayer(at point: CGPoint) {
// if !crossMarkLayer.isHidden {return}
CATransaction.begin()
CATransaction.setDisableActions(true)
stickAnchorLayer.position = point
stickAnchorLayer.isHidden = false
CATransaction.commit()
}
private func createStickThumb() -> CAShapeLayer {
let stickThumbLayer = CAShapeLayer()
stickThumbLayer.path = UIBezierPath(
arcCenter: .zero,
radius: touchPointAnchored ? 10 : stickThumbSize,
startAngle: 0,
endAngle: 2 * .pi,
clockwise: true
).cgPath
layer.addSublayer(stickThumbLayer)
stickThumbLayer.strokeColor = UIColor(white: 0, alpha: 0.3).cgColor
stickThumbLayer.lineWidth = 1.2
stickThumbLayer.shadowOffset = CGSize(width: 0, height: 2)
stickThumbLayer.shadowRadius = 4.5
stickThumbLayer.shadowOpacity = 0.26
stickThumbLayer.fillColor = UIColor(white: 1, alpha: 0.6).cgColor
stickThumbLayer.isHidden = true
return stickThumbLayer
}
private func getStickThumbReady(at point: CGPoint) {
CATransaction.begin()
CATransaction.setDisableActions(true)
stickThumb.position = point
stickThumb.isHidden = false
CATransaction.commit()
}
@objc public func updateStickIndicator() {
CATransaction.begin()
CATransaction.setDisableActions(true)
stickThumb.removeAllAnimations()
if !OnScreenWidgetView.editMode {
if stickThumb.isHidden { stickThumb.isHidden = false }
let realOffsetX = touchInputToStickBallCoord(input: stickOffsetToWeightedTouchInput(offset: stickOffsetVector.dx))
let realOffsetY = touchInputToStickBallCoord(input: stickOffsetToWeightedTouchInput(offset: stickOffsetVector.dy))
let touchPointAnchoredOffset = CGPoint(
x: touchBeganLocation.x + realOffsetX,
y: touchBeganLocation.y - realOffsetY - stickIndicatorOffset
)
let fixedAnchorIndicatorOffset = CGPoint(
x: self.bounds.midX + realOffsetX/(self.sensitivityFactorX*(stickThumbMaxOffset/stickInputScale)),
y: self.bounds.midY - realOffsetY/(self.sensitivityFactorX*(stickThumbMaxOffset/stickInputScale))
)
if touchPointAnchored {
if firstTouchMoved {stickThumb.position = touchPointAnchored ? touchPointAnchoredOffset : fixedAnchorIndicatorOffset}
}
else {stickThumb.position = fixedAnchorIndicatorOffset.isValid ? fixedAnchorIndicatorOffset : CGPoint(x: self.bounds.midX, y: self.bounds.midY)}
} else {
if stickThumb.isHidden { stickThumb.isHidden = false }
stickAnchorLayer.position = touchPointAnchored ? CGPoint(
x: self.bounds.midX,
y: self.bounds.midY - stickIndicatorOffset
) : CGPoint(x: self.bounds.midX, y: self.bounds.midY)
}
CATransaction.commit()
}
private func resetStickIndicator() {
CATransaction.begin()
CATransaction.setAnimationDuration(0.15)
stickThumb.position = touchPointAnchored ? CGPoint(x: touchBeganLocation.x, y: touchBeganLocation.y - stickIndicatorOffset) : CGPoint(x: self.bounds.midX, y: self.bounds.midY)
if !touchPointAnchored {
self.stickThumb.fillColor = UIColor(white: 1, alpha: 0.25).cgColor
self.stickThumb.strokeColor = UIColor(white: 0, alpha: 0.3).cgColor
}
CATransaction.setCompletionBlock {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
if !self.touchBegan {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.stickAnchorLayer.isHidden = self.touchPointAnchored
self.stickThumb.isHidden = self.touchPointAnchored
CATransaction.commit()
}
}
}
CATransaction.commit()
}
private func weightedTouchInputToStickOffset(input: CGFloat) -> CGFloat {
stickMaxOffset * input / stickInputScale
}
private func stickOffsetToWeightedTouchInput(offset: CGFloat) -> CGFloat {
offset * stickInputScale / stickMaxOffset
}
private func touchInputToStickBallCoord(input: CGFloat) -> CGFloat {
if input > stickInputScale {
return stickThumbMaxOffset
}
if input < -stickInputScale {
return -stickThumbMaxOffset
}
return input * (stickThumbMaxOffset / stickInputScale)
}
}