Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
691aa9c899 | ||
|
|
0dd419a5a3 | ||
|
|
1269c46f56 | ||
|
|
f8badd51c5 | ||
|
|
ed8bf5af9e | ||
|
|
b2521de84e | ||
|
|
165b5e6638 | ||
|
|
e742466409 | ||
|
|
201bf5b447 | ||
|
|
7bed913a1e | ||
|
|
e067fd8fa2 | ||
|
|
ba7d5feb81 | ||
|
|
4a9e7ad2a1 | ||
|
|
4b56a44a47 | ||
|
|
bc98b43de2 | ||
|
|
16b3df5279 | ||
|
|
d202f2f6e3 | ||
|
|
142474eac8 | ||
|
|
acc89dba39 | ||
|
|
8cd366d2cd | ||
|
|
2d67c1b22b | ||
|
|
77b0ef55c9 | ||
|
|
d76f404a4c | ||
|
|
d0b45a019f | ||
|
|
0728e999eb | ||
|
|
f325e9a206 | ||
|
|
49b03e65b2 | ||
|
|
2bc18e2f02 | ||
|
|
dadb292676 | ||
|
|
2c9902ce77 | ||
|
|
6d36219e88 | ||
|
|
470862dbe0 | ||
|
|
f6017824ee | ||
|
|
adeb77cf65 | ||
|
|
9bc080815a | ||
|
|
d03eebb5ba | ||
|
|
ca453d8059 | ||
|
|
cb8126d201 | ||
|
|
1626f30bb6 | ||
|
|
47b5ec2678 | ||
|
|
ccd032834d | ||
|
|
c7ffd51cec | ||
|
|
38f8c50d7f | ||
|
|
fd653b87af | ||
|
|
9bf02e477b | ||
|
|
4440c668f1 | ||
|
|
63d5417a2c | ||
|
|
28d9a0faf2 | ||
|
|
04845df09c | ||
|
|
85df511a23 | ||
|
|
4ecb8bfede | ||
|
|
6008865a41 | ||
|
|
02926b2c6b | ||
|
|
49c5fd308e | ||
|
|
7eeff3bc35 | ||
|
|
6e99889681 | ||
|
|
8cbd09fe9a | ||
|
|
edc703c534 | ||
|
|
f85d545c17 | ||
|
|
0830173e59 | ||
|
|
0e21e3a9ed | ||
|
|
6356427454 | ||
|
|
4dc6220f28 | ||
|
|
03fcfaff28 | ||
|
|
d5c96f77cb | ||
|
|
bb6f2f0c0d | ||
|
|
8d54ed9316 | ||
|
|
44123c74d9 | ||
|
|
fdf2548fe3 | ||
|
|
bee72bd854 | ||
|
|
8ec8f32935 | ||
|
|
1d33feec8b | ||
|
|
80af6a9636 | ||
|
|
94eb4dd8a0 | ||
|
|
d3ce55cbd6 | ||
|
|
0b942caa98 | ||
|
|
afc6289a56 | ||
|
|
79684598ab | ||
|
|
b901f2f506 | ||
|
|
14294009f0 | ||
|
|
adbebd23c8 | ||
|
|
3973e13706 | ||
|
|
e56e68dd6f | ||
|
|
c3fcff8d29 | ||
|
|
0152f48886 | ||
|
|
bbd2e37067 | ||
|
|
8377b5062a | ||
|
|
086f1af974 | ||
|
|
652e989927 | ||
|
|
5480e15595 | ||
|
|
375670beb4 | ||
|
|
fa164f0e02 | ||
|
|
a9ede7a904 | ||
|
|
fa32d157ca | ||
|
|
10839f88d8 | ||
|
|
8fa909dea8 | ||
|
|
94250624a8 | ||
|
|
6f264441ab | ||
|
|
03cf463182 | ||
|
|
3e29501b23 | ||
|
|
600b882929 | ||
|
|
be69af925a | ||
|
|
d14ee640c5 | ||
|
|
ac6c688054 | ||
|
|
8f8e86ab4f | ||
|
|
ab1d6bfa90 | ||
|
|
e038601b37 | ||
|
|
008beb9444 | ||
|
|
835d9c9ef6 | ||
|
|
e29e5bd650 | ||
|
|
fecdbec31c | ||
|
|
66fdb607d8 | ||
|
|
6d4f192cbe | ||
|
|
4a59af7c20 | ||
|
|
156b363049 | ||
|
|
c68540c169 | ||
|
|
f89372e265 | ||
|
|
c134dffec2 | ||
|
|
c2d4527d7e | ||
|
|
2071b89d18 | ||
|
|
46902afdc4 | ||
|
|
d23e640977 | ||
|
|
9c18ab4142 | ||
|
|
22e270c4df | ||
|
|
16bd288e70 | ||
|
|
39643153df | ||
|
|
2cf9fdfd40 | ||
|
|
f5226562ae | ||
|
|
32ad5d2f8a | ||
|
|
245f47f660 | ||
|
|
17e3fd07f9 | ||
|
|
cdbe480863 | ||
|
|
189ad76f38 | ||
|
|
d8528fdc81 | ||
|
|
c257a8cab0 | ||
|
|
2912a3a5cb | ||
|
|
75f1d4d392 | ||
|
|
a6159cc785 | ||
|
|
38affb4981 | ||
|
|
e64090d88f | ||
|
|
ef60aeeead | ||
|
|
25112b2c27 | ||
|
|
c0b1326dbd | ||
|
|
f77e533dd6 | ||
|
|
4b001f9366 | ||
|
|
41eadce89a | ||
|
|
46194a5aa0 | ||
|
|
3630880454 | ||
|
|
a59ffacd3a | ||
|
|
03817bcfd7 | ||
|
|
2c4751bef9 | ||
|
|
7caa778ff8 | ||
|
|
3977d342ba | ||
|
|
4e83421104 | ||
|
|
adafacbf81 | ||
|
|
053893005b | ||
|
|
838af88d64 | ||
|
|
573ed07b4f | ||
|
|
aad8673ff0 | ||
|
|
b6a58ea8ab | ||
|
|
8c7bf18b65 | ||
|
|
3823c95582 | ||
|
|
ad0f3dca30 | ||
|
|
5248bbe9c3 | ||
|
|
1262a7b41f | ||
|
|
85611bcab5 | ||
|
|
46b5d9522d | ||
|
|
6dc9ee569b | ||
|
|
9916f64065 | ||
|
|
0ad6b13fae | ||
|
|
5bbdb76d21 | ||
|
|
cbf42b1e52 | ||
|
|
72867d855d | ||
|
|
7d5b984157 | ||
|
|
a3d5771b14 | ||
|
|
1338b4fb85 | ||
|
|
379d965769 | ||
|
|
d7bc73250d | ||
|
|
349daef47f | ||
|
|
138658c635 | ||
|
|
e058839faf | ||
|
|
d4d5146f66 | ||
|
|
85f4838067 | ||
|
|
f23108a3ba | ||
|
|
1623fc34cf | ||
|
|
d244c9bd1f | ||
|
|
e53e997cd4 | ||
|
|
5c646ef3d2 | ||
|
|
f01d36d4a0 | ||
|
|
37fc7e2cb4 | ||
|
|
ea68353328 | ||
|
|
da4bfb851c | ||
|
|
6a1f85975f | ||
|
|
700552bd75 | ||
|
|
5750b99af3 | ||
|
|
cc35e6b3a4 | ||
|
|
3b4800eb78 | ||
|
|
a2855ef522 | ||
|
|
935406b75b | ||
|
|
e933c5de60 | ||
|
|
a23a5491ae | ||
|
|
1ad8f51b7a | ||
|
|
4d1619346d | ||
|
|
df8c273cb9 | ||
|
|
6eca97b4aa | ||
|
|
39f8398b10 | ||
|
|
e32a356cb5 | ||
|
|
e0c556da10 | ||
|
|
a3f9e07cf7 | ||
|
|
ff40e55cd1 | ||
|
|
e548bda73a | ||
|
|
cf11b06f3e | ||
|
|
a799c39aff | ||
|
|
ff230b3433 | ||
|
|
a9f4db52b5 | ||
|
|
071e2a764a | ||
|
|
e050c914bb | ||
|
|
cd9ef74f3f | ||
|
|
644687a50c | ||
|
|
e7132e3371 | ||
|
|
7223ab3be5 | ||
|
|
99952281cb | ||
|
|
9c518dfeb6 | ||
|
|
28e2f13e87 | ||
|
|
4a9e109de5 | ||
|
|
f5b56ca67f | ||
|
|
b7ad4d3562 | ||
|
|
94a8caa5df | ||
|
|
b650ac0ca3 | ||
|
|
9dda594519 | ||
|
|
0ea6248773 | ||
|
|
fee003b824 | ||
|
|
cd415cc639 | ||
|
|
10ceac2ce3 | ||
|
|
86a0ff46ee | ||
|
|
3887ad0c4c | ||
|
|
58f534fcc8 | ||
|
|
f8e14006c1 | ||
|
|
a959a346b0 | ||
|
|
65459b3f4e | ||
|
|
2216899204 | ||
|
|
5fb4887c67 | ||
|
|
6ff2add9dd | ||
|
|
c963c1254d | ||
|
|
8afce40e81 | ||
|
|
e44504bc48 | ||
|
|
11b79dda5b | ||
|
|
8cee8b679c | ||
|
|
1cb9360d83 | ||
|
|
297f5ad7e6 | ||
|
|
3f2a011d7b | ||
|
|
dd0c36bb20 | ||
|
|
aff1bdfe89 | ||
|
|
00533f5d3b | ||
|
|
eff682bc05 | ||
|
|
16a210efa2 | ||
|
|
99a2a4ca1a | ||
|
|
67af68fcf1 | ||
|
|
b96e2d1bdf | ||
|
|
2bb0ce086f | ||
|
|
9bc518dc68 | ||
|
|
f60433ff3f | ||
|
|
b4493707ca | ||
|
|
0fe10d8654 | ||
|
|
87d9590f3b | ||
|
|
eeff9b74da | ||
|
|
ceebd19c0e | ||
|
|
00e0566dd4 | ||
|
|
acd4a04076 | ||
|
|
ac0aa29578 | ||
|
|
9a053d9ed8 | ||
|
|
9124a1fee5 | ||
|
|
858e7c7440 | ||
|
|
e5669e2a30 | ||
|
|
431b32b680 | ||
|
|
7dce11b4c4 | ||
|
|
9fca3a5984 | ||
|
|
733d5c38f3 | ||
|
|
3834b5a604 | ||
|
|
07e36029d0 | ||
|
|
689221f10e | ||
|
|
60dacdf05f | ||
|
|
1614bb0609 | ||
|
|
a386867dd0 | ||
|
|
e177d6eaea | ||
|
|
464492529e | ||
|
|
b950882846 | ||
|
|
63808c9e9c | ||
|
|
76d138d64d | ||
|
|
1232644268 | ||
|
|
2e776baec9 | ||
|
|
8fb4829aef | ||
|
|
fcbc844587 | ||
|
|
aecb1163e3 | ||
|
|
0e51a49898 | ||
|
|
adce543c45 | ||
|
|
83a32f96e6 | ||
|
|
6c72f0331b | ||
|
|
92f1fbf33b | ||
|
|
424fe2c2f0 | ||
|
|
f5d629c62c | ||
|
|
19fa975b6d | ||
|
|
a4951309d3 | ||
|
|
3ec53c29d4 | ||
|
|
3fcaafc42b | ||
|
|
5318acc427 | ||
|
|
daa4fc80eb | ||
|
|
c094d74d8e | ||
|
|
5418936816 | ||
|
|
c3b7679268 | ||
|
|
d12b83d1ef | ||
|
|
32b78d1a4c | ||
|
|
71f66fe8c0 | ||
|
|
d054bbe35f | ||
|
|
bd1c4e3cc9 | ||
|
|
928b765700 | ||
|
|
45acc28384 | ||
|
|
4e65abf6b1 | ||
|
|
d818f8a7ce | ||
|
|
c0850ce9d2 | ||
|
|
c3941df7e9 | ||
|
|
b1b46b3ec0 | ||
|
|
dff6679a90 | ||
|
|
4ab21aed0e | ||
|
|
d67a40f155 | ||
|
|
b506917382 | ||
|
|
944f7422b3 | ||
|
|
4843c046d7 | ||
|
|
61df4e7e61 | ||
|
|
48558316d1 | ||
|
|
5903b358ee | ||
|
|
6881098ca2 | ||
|
|
4d783649f5 | ||
|
|
2b40224b2b | ||
|
|
e4d6be3bb3 | ||
|
|
6c0b1550fd | ||
|
|
967fbe8072 | ||
|
|
c7dd7a2c21 | ||
|
|
49ed0e43e9 | ||
|
|
6bae985e2c | ||
|
|
39d7d9e213 | ||
|
|
eb3501e090 | ||
|
|
83566ad4e1 | ||
|
|
19e78d5eb6 | ||
|
|
31ab333f5b | ||
|
|
ff8cb20cae |
@@ -3,19 +3,40 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 60;
|
||||
objectVersion = 70;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
180345E12E819877004AF36F /* libopus-sim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 180345E02E819877004AF36F /* libopus-sim.a */; };
|
||||
180E6E372E93BFDB0067C73A /* AlertControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */; };
|
||||
1817B61E2E6FD04D000D938D /* SafeTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1817B61D2E6FD04D000D938D /* SafeTimer.swift */; };
|
||||
182760F72E83E2EA00EC1C5C /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 182760F62E83E2EA00EC1C5C /* DequeModule */; };
|
||||
182760FA2E83E57D00EC1C5C /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 182760F92E83E57D00EC1C5C /* DequeModule */; };
|
||||
182760FD2E83E68B00EC1C5C /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 182760FC2E83E68B00EC1C5C /* Collections */; };
|
||||
1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1829CE3E2E2154DF0074A99A /* SceneDelegate.m */; };
|
||||
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 18341A032DEB662C0094E5F2 /* FixedTintImageView.m */; };
|
||||
18416EA42EBB3DCE00AE0124 /* UITouchUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */; };
|
||||
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850C8292E667E4000845308 /* MicHandler.swift */; };
|
||||
18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18729EDD2E8BD68800101194 /* UUIDHelper.swift */; };
|
||||
18780CC72F10D66000B90483 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 18780CC62F10D66000B90483 /* StoreKit.framework */; };
|
||||
18780CCA2F10D6CC00B90483 /* IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18780CC92F10D6CC00B90483 /* IAPManager.swift */; };
|
||||
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */; };
|
||||
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1912E24C33100108730 /* AboutViewController.swift */; };
|
||||
1888C1932E24C41000108730 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1902E24C2C800108730 /* AboutView.swift */; };
|
||||
188973D32E386A670025E48C /* widgetTemplatesIPhone.bin in Resources */ = {isa = PBXBuildFile; fileRef = 188973D22E386A670025E48C /* widgetTemplatesIPhone.bin */; };
|
||||
18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */; };
|
||||
189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 189AD2992DE7446000E24B88 /* HostCollectionViewController.m */; };
|
||||
18A0EE252EFB9B5A00BA853A /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = 18A0EE242EFB9B5A00BA853A /* SVGKit */; };
|
||||
18A0EE272EFB9B5A00BA853A /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 18A0EE262EFB9B5A00BA853A /* SVGKitSwift */; };
|
||||
18A0EE2C2EFBCCFD00BA853A /* GraphicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */; };
|
||||
18A765BD2ECD51FB004784AA /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; };
|
||||
18B317032EE67A3A009B62C2 /* InertialScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B317022EE67A39009B62C2 /* InertialScroller.swift */; };
|
||||
18D43D7F2F050240008C326F /* PressureCurve.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D43D7E2F050240008C326F /* PressureCurve.swift */; };
|
||||
18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E4BCE92E891895000A8657 /* MotionHandler.swift */; };
|
||||
18FCADAF2ECAC23200787A96 /* PencilHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FCADAE2ECAC23200787A96 /* PencilHandler.swift */; };
|
||||
2A7113FD2F5A6C65001D852D /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7113FC2F5A6C65001D852D /* ThemeManager.swift */; };
|
||||
2AC1E3832F507D80004F76C0 /* GenericUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC1E3822F507D80004F76C0 /* GenericUtils.swift */; };
|
||||
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; };
|
||||
841BAC922DE35D6B00DA639E /* ThemeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 841BAC912DE35D6B00DA639E /* ThemeManager.m */; };
|
||||
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */; };
|
||||
84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B072C29ACCA00A77989 /* LayoutOnScreenControls.m */; };
|
||||
84351B0F2C29ACCB00A77989 /* OnScreenButtonState.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B092C29ACCA00A77989 /* OnScreenButtonState.m */; };
|
||||
@@ -151,23 +172,38 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
180345E02E819877004AF36F /* libopus-sim.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libopus-sim.a"; sourceTree = "<group>"; };
|
||||
180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertControllerUtil.swift; sourceTree = "<group>"; };
|
||||
1817B61D2E6FD04D000D938D /* SafeTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeTimer.swift; sourceTree = "<group>"; };
|
||||
1829CE3D2E2154DF0074A99A /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = "<group>"; };
|
||||
1829CE3E2E2154DF0074A99A /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = "<group>"; };
|
||||
18341A022DEB65AB0094E5F2 /* FixedTintImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FixedTintImageView.h; sourceTree = "<group>"; };
|
||||
18341A032DEB662C0094E5F2 /* FixedTintImageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FixedTintImageView.m; sourceTree = "<group>"; };
|
||||
18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITouchUtil.swift; sourceTree = "<group>"; };
|
||||
1850C8292E667E4000845308 /* MicHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicHandler.swift; sourceTree = "<group>"; };
|
||||
18729EDD2E8BD68800101194 /* UUIDHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDHelper.swift; sourceTree = "<group>"; };
|
||||
18780CC62F10D66000B90483 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||
18780CC92F10D6CC00B90483 /* IAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPManager.swift; sourceTree = "<group>"; };
|
||||
187F4B232E04633100EECC59 /* WidgetPanelStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WidgetPanelStackView.h; sourceTree = "<group>"; };
|
||||
187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WidgetPanelStackView.m; sourceTree = "<group>"; };
|
||||
1888C1902E24C2C800108730 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
||||
1888C1912E24C33100108730 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
|
||||
188973D22E386A670025E48C /* widgetTemplatesIPhone.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = widgetTemplatesIPhone.bin; path = PresetConfigs/widgetTemplatesIPhone.bin; sourceTree = "<group>"; };
|
||||
18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchPadGestureHandler.swift; sourceTree = "<group>"; };
|
||||
189AD2962DE743B700E24B88 /* HostCollectionViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HostCollectionViewController.h; sourceTree = "<group>"; };
|
||||
189AD2992DE7446000E24B88 /* HostCollectionViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HostCollectionViewController.m; sourceTree = "<group>"; };
|
||||
18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicUtils.swift; sourceTree = "<group>"; };
|
||||
18A765BC2ECD51FB004784AA /* ControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerUtil.swift; sourceTree = "<group>"; };
|
||||
18B317022EE67A39009B62C2 /* InertialScroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InertialScroller.swift; sourceTree = "<group>"; };
|
||||
18BF141D2E0D7A00009DEE4F /* VoidLink v1.0.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "VoidLink v1.0.xcdatamodel"; sourceTree = "<group>"; };
|
||||
18D43D7E2F050240008C326F /* PressureCurve.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PressureCurve.swift; sourceTree = "<group>"; };
|
||||
18E4BCE92E891895000A8657 /* MotionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionHandler.swift; sourceTree = "<group>"; };
|
||||
18FCADAE2ECAC23200787A96 /* PencilHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PencilHandler.swift; sourceTree = "<group>"; };
|
||||
2A7113FC2F5A6C65001D852D /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
|
||||
2AC1E3822F507D80004F76C0 /* GenericUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericUtils.swift; sourceTree = "<group>"; };
|
||||
693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
||||
840751B12BF49B5A0096B657 /* CustomTapGestureRecognizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomTapGestureRecognizer.h; sourceTree = "<group>"; };
|
||||
840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomTapGestureRecognizer.m; sourceTree = "<group>"; };
|
||||
841BAC8F2DE35BEC00DA639E /* ThemeManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ThemeManager.h; sourceTree = "<group>"; };
|
||||
841BAC912DE35D6B00DA639E /* ThemeManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ThemeManager.m; sourceTree = "<group>"; };
|
||||
84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnScreenWidgetView.swift; sourceTree = "<group>"; };
|
||||
84351B062C29ACCA00A77989 /* OnScreenButtonState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OnScreenButtonState.h; sourceTree = "<group>"; };
|
||||
84351B072C29ACCA00A77989 /* LayoutOnScreenControls.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LayoutOnScreenControls.m; sourceTree = "<group>"; };
|
||||
@@ -378,23 +414,34 @@
|
||||
FBDE86E519F82297001C18A8 /* UIAppView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIAppView.m; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
18A0EE282EFBCBFB00BA853A /* SVGs */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SVGs; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
FB290CEB19B2C406004C83CF /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
18A0EE272EFB9B5A00BA853A /* SVGKitSwift in Frameworks */,
|
||||
18A0EE252EFB9B5A00BA853A /* SVGKit in Frameworks */,
|
||||
9890CF6B203B7EE1006C4B06 /* libxml2.tbd in Frameworks */,
|
||||
98CFB82F1CAD481B0048EF74 /* libmoonlight-common.a in Frameworks */,
|
||||
FB8946ED19F6AFE800339C8A /* libopus.a in Frameworks */,
|
||||
98882A082AF60F7400C5A11C /* libavutil.a in Frameworks */,
|
||||
180345E12E819877004AF36F /* libopus-sim.a in Frameworks */,
|
||||
182760F72E83E2EA00EC1C5C /* DequeModule in Frameworks */,
|
||||
182760FD2E83E68B00EC1C5C /* Collections in Frameworks */,
|
||||
FB290CF419B2C406004C83CF /* CoreGraphics.framework in Frameworks */,
|
||||
FB290CF819B2C406004C83CF /* CoreData.framework in Frameworks */,
|
||||
98181BED2791281100E43572 /* CoreMotion.framework in Frameworks */,
|
||||
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */,
|
||||
98882A072AF60F7200C5A11C /* libavformat.a in Frameworks */,
|
||||
FB290CF619B2C406004C83CF /* UIKit.framework in Frameworks */,
|
||||
18780CC72F10D66000B90483 /* StoreKit.framework in Frameworks */,
|
||||
FB290CF219B2C406004C83CF /* Foundation.framework in Frameworks */,
|
||||
FB4679011A57048000377732 /* CoreFoundation.framework in Frameworks */,
|
||||
182760FA2E83E57D00EC1C5C /* DequeModule in Frameworks */,
|
||||
98181BE92791277400E43572 /* libSDL2.a in Frameworks */,
|
||||
98882A062AF60F7000C5A11C /* libavcodec.a in Frameworks */,
|
||||
);
|
||||
@@ -559,6 +606,7 @@
|
||||
FB290CF019B2C406004C83CF /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18780CC62F10D66000B90483 /* StoreKit.framework */,
|
||||
98882A032AF60F5300C5A11C /* libavcodec.a */,
|
||||
98882A052AF60F5300C5A11C /* libavformat.a */,
|
||||
98882A0A2AF60F9000C5A11C /* libavcodec.a */,
|
||||
@@ -612,16 +660,17 @@
|
||||
FB290D0319B2C406004C83CF /* AppDelegate.m */,
|
||||
848F45682DDCD0D600581E36 /* HostCardView.h */,
|
||||
848F45692DDCD12500581E36 /* HostCardView.m */,
|
||||
841BAC8F2DE35BEC00DA639E /* ThemeManager.h */,
|
||||
841BAC912DE35D6B00DA639E /* ThemeManager.m */,
|
||||
2A7113FC2F5A6C65001D852D /* ThemeManager.swift */,
|
||||
847D2FD82DDA00C800937A62 /* MenuSectionView.h */,
|
||||
847D2FDA2DDA015F00937A62 /* MenuSectionView.m */,
|
||||
FBDE86E419F82297001C18A8 /* UIAppView.h */,
|
||||
FBDE86E519F82297001C18A8 /* UIAppView.m */,
|
||||
180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */,
|
||||
1888C1902E24C2C800108730 /* AboutView.swift */,
|
||||
1888C1912E24C33100108730 /* AboutViewController.swift */,
|
||||
FB89463719F6473800339C8A /* Launch Screen.xib */,
|
||||
FB290D0819B2C406004C83CF /* Images.xcassets */,
|
||||
18A0EE282EFBCBFB00BA853A /* SVGs */,
|
||||
FB290D0519B2C406004C83CF /* Limelight.xcdatamodeld */,
|
||||
FB1D59981BBCCD7E00F482CA /* AppCollectionView.h */,
|
||||
FB1D59991BBCCD7E00F482CA /* AppCollectionView.m */,
|
||||
@@ -677,18 +726,27 @@
|
||||
9827E7A7251436EA00F25707 /* HapticContext.h */,
|
||||
840751B12BF49B5A0096B657 /* CustomTapGestureRecognizer.h */,
|
||||
840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */,
|
||||
9819CC12254F107A008A7C8E /* RelativeTouchHandler.h */,
|
||||
843CC7F42C1E778000BBC475 /* NativeTouchHandler.h */,
|
||||
844CD0E82C7F847E004861A7 /* PureNativeTouchHandler.h */,
|
||||
844CD0EA2C7F8500004861A7 /* PureNativeTouchHandler.m */,
|
||||
843CC7F42C1E778000BBC475 /* NativeTouchHandler.h */,
|
||||
843CC7F52C1E7A1700BBC475 /* NativeTouchHandler.m */,
|
||||
9819CC12254F107A008A7C8E /* RelativeTouchHandler.h */,
|
||||
9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */,
|
||||
9819CC20254F180F008A7C8E /* AbsoluteTouchHandler.h */,
|
||||
9819CC21254F180F008A7C8E /* AbsoluteTouchHandler.m */,
|
||||
988FCD3F293B091B003050E2 /* KeyboardInputField.h */,
|
||||
988FCD40293B091B003050E2 /* KeyboardInputField.m */,
|
||||
18D43D7E2F050240008C326F /* PressureCurve.swift */,
|
||||
18FCADAE2ECAC23200787A96 /* PencilHandler.swift */,
|
||||
18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */,
|
||||
18B317022EE67A39009B62C2 /* InertialScroller.swift */,
|
||||
18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */,
|
||||
18A765BC2ECD51FB004784AA /* ControllerUtil.swift */,
|
||||
1850C8292E667E4000845308 /* MicHandler.swift */,
|
||||
18E4BCE92E891895000A8657 /* MotionHandler.swift */,
|
||||
84E1CDEB2C4F67D300547904 /* CommandManager.swift */,
|
||||
84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */,
|
||||
1817B61D2E6FD04D000D938D /* SafeTimer.swift */,
|
||||
);
|
||||
path = Input;
|
||||
sourceTree = "<group>";
|
||||
@@ -728,6 +786,10 @@
|
||||
FB89461E19F646E200339C8A /* Utility */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18729EDD2E8BD68800101194 /* UUIDHelper.swift */,
|
||||
2AC1E3822F507D80004F76C0 /* GenericUtils.swift */,
|
||||
18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */,
|
||||
18780CC92F10D6CC00B90483 /* IAPManager.swift */,
|
||||
97E20DD42E2164A9000D5DCA /* BandwidthTracker.h */,
|
||||
97E20DD52E2164A9000D5DCA /* BandwidthTracker.m */,
|
||||
97E20DD62E2164A9000D5DCA /* FloatBuffer.h */,
|
||||
@@ -809,6 +871,7 @@
|
||||
FB8946E919F6AFB800339C8A /* lib */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
180345E02E819877004AF36F /* libopus-sim.a */,
|
||||
FB8946EA19F6AFB800339C8A /* libopus.a */,
|
||||
);
|
||||
path = lib;
|
||||
@@ -888,6 +951,9 @@
|
||||
dependencies = (
|
||||
98AB2E861CAD468B0089BB98 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
18A0EE282EFBCBFB00BA853A /* SVGs */,
|
||||
);
|
||||
name = VoidLink;
|
||||
productName = Limelight;
|
||||
productReference = FB290CEE19B2C406004C83CF /* VoidLink.app */;
|
||||
@@ -922,6 +988,8 @@
|
||||
mainGroup = FB290CE519B2C406004C83CF;
|
||||
packageReferences = (
|
||||
F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */,
|
||||
182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */,
|
||||
18A0EE232EFB9B5A00BA853A /* XCRemoteSwiftPackageReference "SVGKit" */,
|
||||
);
|
||||
productRefGroup = FB290CEF19B2C406004C83CF /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -985,7 +1053,9 @@
|
||||
FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */,
|
||||
97E20DDF2E2164A9000D5DCA /* Frame.m in Sources */,
|
||||
97E20DE02E2164A9000D5DCA /* FrameQueue.m in Sources */,
|
||||
18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */,
|
||||
97E20DE12E2164A9000D5DCA /* FloatBuffer.m in Sources */,
|
||||
18FCADAF2ECAC23200787A96 /* PencilHandler.swift in Sources */,
|
||||
97E20DE22E2164A9000D5DCA /* PaddedLabel.m in Sources */,
|
||||
97E20DE32E2164A9000D5DCA /* BandwidthTracker.m in Sources */,
|
||||
84591A512BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m in Sources */,
|
||||
@@ -1005,8 +1075,10 @@
|
||||
97E21BC12E21882D000D5DCA /* imgui_widgets.cpp in Sources */,
|
||||
97E21BC22E21882D000D5DCA /* imgui.cpp in Sources */,
|
||||
97E21BC32E21882D000D5DCA /* imgui_draw.cpp in Sources */,
|
||||
18A765BD2ECD51FB004784AA /* ControllerUtil.swift in Sources */,
|
||||
84E1CDEC2C4F67D300547904 /* CommandManager.swift in Sources */,
|
||||
847D2FDB2DDA015F00937A62 /* MenuSectionView.m in Sources */,
|
||||
18A0EE2C2EFBCCFD00BA853A /* GraphicUtils.swift in Sources */,
|
||||
FB53E1431BE5DC4400CD6ECE /* IdManager.m in Sources */,
|
||||
189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */,
|
||||
9897B6A1221260EF00966419 /* VoidController.m in Sources */,
|
||||
@@ -1014,14 +1086,18 @@
|
||||
FB89462919F646E200339C8A /* mkcert.c in Sources */,
|
||||
FB9AFD281A7C84ED00872C98 /* HttpResponse.m in Sources */,
|
||||
FB89463019F646E200339C8A /* StreamConfiguration.m in Sources */,
|
||||
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */,
|
||||
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */,
|
||||
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */,
|
||||
2AC1E3832F507D80004F76C0 /* GenericUtils.swift in Sources */,
|
||||
97E21BB52E2186A3000D5DCA /* imgui_impl_metal.mm in Sources */,
|
||||
9777AAF32E2CF7A400D7FBAF /* Shaders-Linear.metal in Sources */,
|
||||
84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */,
|
||||
97E20DEA2E216532000D5DCA /* SceneDelegate.m in Sources */,
|
||||
1817B61E2E6FD04D000D938D /* SafeTimer.swift in Sources */,
|
||||
FBD3495019FF2174002D2A60 /* SettingsViewController.m in Sources */,
|
||||
843CC7F02C1E757D00BBC475 /* NativeTouchPointer.m in Sources */,
|
||||
2A7113FD2F5A6C65001D852D /* ThemeManager.swift in Sources */,
|
||||
FB89462C19F646E200339C8A /* HttpManager.m in Sources */,
|
||||
FB89462D19F646E200339C8A /* MDNSManager.m in Sources */,
|
||||
84351B0F2C29ACCB00A77989 /* OnScreenButtonState.m in Sources */,
|
||||
@@ -1029,7 +1105,8 @@
|
||||
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */,
|
||||
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */,
|
||||
FB4678FA1A55FFAD00377732 /* DiscoveryManager.m in Sources */,
|
||||
841BAC922DE35D6B00DA639E /* ThemeManager.m in Sources */,
|
||||
18D43D7F2F050240008C326F /* PressureCurve.swift in Sources */,
|
||||
180E6E372E93BFDB0067C73A /* AlertControllerUtil.swift in Sources */,
|
||||
98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */,
|
||||
FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */,
|
||||
FBD1C8E21A8AD71400C6703C /* Logger.m in Sources */,
|
||||
@@ -1048,6 +1125,7 @@
|
||||
84351B2C2C29AD6100A77989 /* OSCProfilesTableViewController.m in Sources */,
|
||||
98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */,
|
||||
1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */,
|
||||
18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */,
|
||||
FB89462819F646E200339C8A /* CryptoManager.m in Sources */,
|
||||
DC1F5A07206436B20037755F /* ConnectionHelper.m in Sources */,
|
||||
FB89462E19F646E200339C8A /* PairManager.m in Sources */,
|
||||
@@ -1065,13 +1143,17 @@
|
||||
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */,
|
||||
FBD349621A0089F6002D2A60 /* DataManager.m in Sources */,
|
||||
97E20DE72E216510000D5DCA /* ImGuiRenderer.mm in Sources */,
|
||||
18B317032EE67A3A009B62C2 /* InertialScroller.swift in Sources */,
|
||||
FB4A23B81A9D3637004D2EF2 /* LoadingFrameViewController.m in Sources */,
|
||||
FB9AFD3A1A7E05CE00872C98 /* ServerInfoResponse.m in Sources */,
|
||||
84351B2B2C29AD6100A77989 /* LayoutOnScreenControlsViewController.m in Sources */,
|
||||
84351B5C2C29ADBE00A77989 /* ProfileTableViewCell.m in Sources */,
|
||||
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */,
|
||||
18416EA42EBB3DCE00AE0124 /* UITouchUtil.swift in Sources */,
|
||||
FB89463119F646E200339C8A /* StreamManager.m in Sources */,
|
||||
18780CCA2F10D6CC00B90483 /* IAPManager.swift in Sources */,
|
||||
84351B2D2C29AD6100A77989 /* ToolBarContainerView.m in Sources */,
|
||||
18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */,
|
||||
988FCD41293B091B003050E2 /* KeyboardInputField.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1260,7 +1342,7 @@
|
||||
"$(PROJECT_DIR)/Limelight/Database",
|
||||
);
|
||||
INFOPLIST_FILE = "VoidLink/Limelight-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "VoidLink - beta";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = VoidLink;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -1271,6 +1353,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-32",
|
||||
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-64",
|
||||
"$(PROJECT_DIR)/libs/opus/lib",
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
||||
"$(inherited)",
|
||||
@@ -1325,7 +1408,7 @@
|
||||
"$(PROJECT_DIR)/Limelight/Database",
|
||||
);
|
||||
INFOPLIST_FILE = "VoidLink/Limelight-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "VoidLink - beta";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = VoidLink;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -1336,6 +1419,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-32",
|
||||
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-64",
|
||||
"$(PROJECT_DIR)/libs/opus/lib",
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
||||
"$(inherited)",
|
||||
@@ -1388,6 +1472,22 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/apple/swift-collections.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.1.4;
|
||||
};
|
||||
};
|
||||
18A0EE232EFB9B5A00BA853A /* XCRemoteSwiftPackageReference "SVGKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SVGKit/SVGKit.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 3.0.0;
|
||||
};
|
||||
};
|
||||
F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/krzyzanowskim/OpenSSL-Package.git";
|
||||
@@ -1399,6 +1499,31 @@
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
182760F62E83E2EA00EC1C5C /* DequeModule */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||
productName = DequeModule;
|
||||
};
|
||||
182760F92E83E57D00EC1C5C /* DequeModule */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||
productName = DequeModule;
|
||||
};
|
||||
182760FC2E83E68B00EC1C5C /* Collections */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||
productName = Collections;
|
||||
};
|
||||
18A0EE242EFB9B5A00BA853A /* SVGKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 18A0EE232EFB9B5A00BA853A /* XCRemoteSwiftPackageReference "SVGKit" */;
|
||||
productName = SVGKit;
|
||||
};
|
||||
18A0EE262EFB9B5A00BA853A /* SVGKitSwift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 18A0EE232EFB9B5A00BA853A /* XCRemoteSwiftPackageReference "SVGKit" */;
|
||||
productName = SVGKitSwift;
|
||||
};
|
||||
F74BEF9B2C1A705600224667 /* OpenSSL */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */;
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
{
|
||||
"originHash" : "f17bf6f1131c7255a75ffd6a989b69573cb49c332e612dffb429905d8d7afe72",
|
||||
"originHash" : "d1fc50a28d75017e32553df2b233f8a7e2cf1542da32728ce6722b200f745608",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "cocoalumberjack",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git",
|
||||
"state" : {
|
||||
"revision" : "a9ed4b6f9bdedce7d77046f43adfb8ce1fd54114",
|
||||
"version" : "3.9.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "openssl-package",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -9,6 +18,33 @@
|
||||
"revision" : "b9eb055fdf73e595cb4b9f665d13dc85975bf80a",
|
||||
"version" : "3.3.2000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "svgkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SVGKit/SVGKit.git",
|
||||
"state" : {
|
||||
"revision" : "58152b9f7c85eab239160b36ffdfd364aa43d666",
|
||||
"version" : "3.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-collections",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-collections.git",
|
||||
"state" : {
|
||||
"revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
|
||||
"version" : "1.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-log",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-log",
|
||||
"state" : {
|
||||
"revision" : "bc386b95f2a16ccd0150a8235e7c69eab2b866ca",
|
||||
"version" : "1.8.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
|
||||
@@ -16,12 +16,12 @@ public struct AboutView: View {
|
||||
public var aboutVC:UIViewController
|
||||
|
||||
public var body: some View {
|
||||
VStack(spacing: 10) {
|
||||
VStack(spacing: 7) {
|
||||
// App 图标
|
||||
Image(uiImage: UIImage(named: "AppIconMedium") ?? UIImage())
|
||||
// .resizable()
|
||||
.resizable(capInsets:EdgeInsets(top: 3.5, leading: 3.5, bottom: 3.5, trailing: 3.5))
|
||||
.frame(width: 100, height: 100)
|
||||
.frame(width: 90, height: 90)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 28))
|
||||
|
||||
// App 名称
|
||||
@@ -36,30 +36,40 @@ public struct AboutView: View {
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
if Locale.preferredLanguages.first!.hasPrefix("zh-Hans") {
|
||||
if #available(iOS 14.0, *) {
|
||||
Text("闽ICP备17012590号-3A 主办单位:福州创图信息技术有限公司").font(.caption2)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(nil)
|
||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||
//.padding()
|
||||
} else {
|
||||
Text("闽ICP备17012590号-3A 主办单位:福州创图信息技术有限公司").font(.caption)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(nil)
|
||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||
//.padding()
|
||||
}
|
||||
}
|
||||
|
||||
// 说明文字
|
||||
Text(SwiftLocalizationHelper.localizedString(forKey: "From the player community, to the player community."))
|
||||
.multilineTextAlignment(.center)
|
||||
.font(Font.caption.italic())
|
||||
.lineLimit(nil)
|
||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||
.padding()
|
||||
|
||||
|
||||
Text(SwiftLocalizationHelper.localizedString(forKey: "VoidLink delivers better performance now!"))
|
||||
.multilineTextAlignment(.center)
|
||||
.font(Font.callout.bold())
|
||||
.lineLimit(nil)
|
||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||
|
||||
if Locale.preferredLanguages.first!.hasPrefix("zh-Hans") {
|
||||
if #available(iOS 14.0, *) {
|
||||
Text("闽ICP备17012590号-3A\n主办单位:福州创图信息技术有限公司").font(.caption2)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(nil)
|
||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||
.padding()
|
||||
} else {
|
||||
Text("闽ICP备17012590号-3A\n主办单位:福州创图信息技术有限公司").font(.caption)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(nil)
|
||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
// 链接按钮
|
||||
if #available(iOS 14.0, *) {
|
||||
Link(SwiftLocalizationHelper.localizedString(forKey: "Join us"), destination: URL(string: SwiftLocalizationHelper.localizedString(forKey: "supportLink"))!)
|
||||
Link(SwiftLocalizationHelper.localizedString(forKey: "Learn more & join us"), destination: URL(string: SwiftLocalizationHelper.localizedString(forKey: "supportLink"))!)
|
||||
.padding(.top, 10)
|
||||
Spacer()
|
||||
// OK 按钮
|
||||
|
||||
@@ -29,7 +29,7 @@ import SwiftUI
|
||||
])
|
||||
|
||||
// 🎯 设置弹窗大小
|
||||
preferredContentSize = CGSize(width: 530, height: 450)
|
||||
preferredContentSize = CGSize(width: 530, height: 430)
|
||||
|
||||
// ⚠️ 一定要设置以下样式才会生效 preferredContentSize
|
||||
modalPresentationStyle = .formSheet
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// AlertControllerUtil.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by Weimin on 2025/10/6.
|
||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
import UIKit
|
||||
|
||||
@objc class AlertControllerUtil: NSObject {
|
||||
|
||||
/// 类方法,直接在任何 UIViewController 上显示倒计时弹窗
|
||||
/// - Parameters:
|
||||
/// - viewController: 要显示弹窗的控制器
|
||||
/// - title: 弹窗标题
|
||||
/// - message: 弹窗内容
|
||||
/// - buttonTitle: 按钮初始文字(倒计时结束后显示)
|
||||
/// - countdown: 倒计时秒数
|
||||
/// - completion: 点击确认或倒计时结束的回调
|
||||
|
||||
@objc public static var actionCancelled:Bool = false
|
||||
@objc public static var alertController:UIAlertController = UIAlertController()
|
||||
@objc public static var cancelButtonString:String = "Cancel"
|
||||
@objc public static var autoCompletion:Bool = false
|
||||
|
||||
@objc class func showAlert(
|
||||
in viewController: UIViewController,
|
||||
title: String?,
|
||||
message: String?,
|
||||
withCancel: Bool,
|
||||
buttonTitle: String,
|
||||
countdown: Int,
|
||||
action: (() -> Void)? = nil,
|
||||
completion: (() -> Void)? = nil
|
||||
) {
|
||||
var remainingSeconds = countdown
|
||||
|
||||
alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
|
||||
let confirmAction = UIAlertAction(title: "\(remainingSeconds)", style: .default) { _ in
|
||||
actionCancelled = false
|
||||
completion?()
|
||||
cancelButtonString = "Cancel"
|
||||
}
|
||||
|
||||
let cancelAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: cancelButtonString), style: .cancel) { _ in
|
||||
actionCancelled = true
|
||||
completion?()
|
||||
cancelButtonString = "Cancel"
|
||||
}
|
||||
|
||||
confirmAction.isEnabled = false
|
||||
|
||||
if withCancel {alertController.addAction(cancelAction)}
|
||||
if buttonTitle != "" && !autoCompletion {alertController.addAction(confirmAction)}
|
||||
|
||||
if(countdown == 0) {
|
||||
confirmAction.setValue(buttonTitle, forKey: "title")
|
||||
confirmAction.isEnabled = buttonTitle != ""
|
||||
}
|
||||
|
||||
viewController.present(alertController, animated: true, completion: action)
|
||||
|
||||
if(countdown == 0) {return}
|
||||
|
||||
// 倒计时
|
||||
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
|
||||
timer.schedule(deadline: .now(), repeating: 1.0, leeway: .milliseconds(50))
|
||||
timer.setEventHandler {
|
||||
remainingSeconds -= 1
|
||||
|
||||
if remainingSeconds <= 0 {
|
||||
timer.cancel()
|
||||
confirmAction.isEnabled = true
|
||||
confirmAction.setValue(buttonTitle, forKey: "title")
|
||||
if autoCompletion {
|
||||
autoCompletion = false
|
||||
completion?()
|
||||
alertController.dismiss(animated: false)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
confirmAction.setValue("\(remainingSeconds)", forKey: "title")
|
||||
}
|
||||
}
|
||||
timer.resume()
|
||||
}
|
||||
}
|
||||
@@ -16,15 +16,17 @@
|
||||
|
||||
@interface DataManager : NSObject
|
||||
|
||||
typedef NS_ENUM(NSUInteger, UINavigationBarHeight) {
|
||||
UINavigationBarHeightIPad = 50,
|
||||
UINavigationBarHeightIPhone = 44
|
||||
typedef NS_ENUM(NSInteger, ControllerGyroSwitchMode) {
|
||||
ControllerGyroSwitchDisabled,
|
||||
ControllerGyroSwitchPressToToggle,
|
||||
ControllerGyroSwitchHoldDown
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, TouchMode) {
|
||||
RelativeTouch,
|
||||
NativeTouch,
|
||||
AbsoluteTouch,
|
||||
TouchDisabled,
|
||||
NativeTouchOnly
|
||||
};
|
||||
|
||||
@@ -42,7 +44,20 @@ typedef NS_ENUM(NSInteger, GyroMode) {
|
||||
AlwaysController
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, HapticEnginePreference) {
|
||||
HapticEngineAuto,
|
||||
RumbleDevice,
|
||||
LeftRightSwapped,
|
||||
RumbleOff
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, ControllerMouseStick) {
|
||||
LeftStickToMouse,
|
||||
RightStickToMouse,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, FramePacingMode) {
|
||||
FramePacingModeOff,
|
||||
FramePacingModeLegacy,
|
||||
FramePacingModeQueue
|
||||
};
|
||||
@@ -53,7 +68,18 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
|
||||
RemoveSettingItem,
|
||||
};
|
||||
|
||||
- (void) saveSettingsWithBitrate:(NSInteger)bitrate
|
||||
typedef NS_ENUM(NSInteger, WidgetSizeTransition) {
|
||||
keepWidgetSize,
|
||||
transitionWithOrientation,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, PencilTickMode) {
|
||||
PencilTickDisabled,
|
||||
ManualTick
|
||||
};
|
||||
|
||||
- (void) saveSettings:(Settings*)settings
|
||||
withBitrate:(NSInteger)bitrate
|
||||
framerate:(NSInteger)framerate
|
||||
height:(NSInteger)height
|
||||
width:(NSInteger)width
|
||||
@@ -69,25 +95,32 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
|
||||
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
|
||||
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
|
||||
gyroSensitivity:(CGFloat)gyroSensitivity
|
||||
localVolume:(CGFloat)localVolume
|
||||
micVolume:(CGFloat)micVolume
|
||||
touchMoveEventInterval:(NSInteger)touchMoveEventInterval
|
||||
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
|
||||
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
|
||||
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
|
||||
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
|
||||
showKeyboardToolbar:(BOOL)showKeyboardToolbar
|
||||
optimizeGames:(BOOL)optimizeGames
|
||||
multiController:(BOOL)multiController
|
||||
buttonVisualFeedback:(BOOL)buttonVisualFeedback
|
||||
touchPointTracking:(BOOL)touchPointTracking
|
||||
swapABXYButtons:(BOOL)swapABXYButtons
|
||||
audioOnPC:(BOOL)audioOnPC
|
||||
redirectMic:(BOOL)redirectMic
|
||||
useBuiltinMic:(BOOL)useBuiltinMic
|
||||
preferredCodec:(uint32_t)preferredCodec
|
||||
enableYUV444:(BOOL)enableYUV444
|
||||
enableYUV444:(BOOL)enableYUV444
|
||||
enablePIP:(BOOL)enablePIP
|
||||
fullColorRange:(BOOL)fullColorRange
|
||||
enableHdr:(BOOL)enableHdr
|
||||
btMouseSupport:(BOOL)btMouseSupport
|
||||
// absoluteTouchMode:(BOOL)absoluteTouchMode
|
||||
touchMode:(NSInteger)touchMode
|
||||
statsOverlayLevel:(NSInteger)statsOverlayLevel
|
||||
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
||||
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
||||
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
||||
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
||||
resolutionSelected:(NSInteger)resolutionSelected
|
||||
externalDisplayMode:(NSInteger)externalDisplayMode
|
||||
localMousePointerMode:(NSInteger)localMousePointerMode
|
||||
@@ -95,8 +128,32 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
|
||||
enableGraphs:(BOOL)enableGraphs
|
||||
graphOpacity:(NSInteger)graphOpacity
|
||||
renderingBackend:(NSInteger)renderingBackend
|
||||
framePacingMode:(NSInteger)framePacingMode
|
||||
backgroundSessionTimer:(NSInteger)backgroundSessionTimer;
|
||||
framePacingMode:(NSInteger)framePacingMode
|
||||
sendDummyEvent:(BOOL)sendDummyEvent
|
||||
rememberFoldState:(BOOL)rememberFoldState
|
||||
singleTapSensitivy:(CGFloat)singleTapSensitivy
|
||||
hapticEngine:(NSInteger)hapticEngine
|
||||
edgeSlidingSensitivity:(CGFloat)edgeSlidingSensitivity
|
||||
audioEngine:(NSInteger)audioEngine
|
||||
delayLeftClick:(BOOL)delayLeftClick
|
||||
duckOtherApps:(BOOL)duckOtherApps
|
||||
muteInBackground:(BOOL)muteInBackground
|
||||
relativeTouchSlideThreshold:(CGFloat)relativeTouchSlideThreshold
|
||||
enablePinch:(BOOL)enablePinch
|
||||
scrollSensitivity:(CGFloat)scrollSensitivity
|
||||
pinchSensitivity:(CGFloat)pinchSensitivity
|
||||
ctrlDownForPinch:(BOOL)ctrlDownForPinch
|
||||
leftClickDelayMs:(CGFloat)leftClickDelayMs
|
||||
settingsMenuOffset:(CGFloat)settingsMenuOffset
|
||||
passthroughGestures:(BOOL)passthroughGestures
|
||||
mapControllerToMouse:(BOOL)mapControllerToMouse
|
||||
controllerMousePointerVelocity:(CGFloat)controllerMousePointerVelocity
|
||||
controllerMouseExpo:(CGFloat)controllerMouseExpo
|
||||
controllerGyroSwitchMode:(NSInteger)controllerGyroSwitchMode
|
||||
enableFrameTimebase:(BOOL)enableFrameTimebase
|
||||
asyncFrameDequeue:(BOOL)asyncFrameDequeue
|
||||
sdrPerformanceWorkaround:(BOOL)sdrPerformanceWorkaround
|
||||
backgroundSessionTimer:(NSInteger)backgroundSessionTimer;
|
||||
|
||||
- (NSArray*) getHosts;
|
||||
- (void) updateHost:(TemporaryHost*)host;
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
return uid;
|
||||
}
|
||||
|
||||
- (void) saveSettingsWithBitrate:(NSInteger)bitrate
|
||||
- (void) saveSettings:(Settings*)settings
|
||||
withBitrate:(NSInteger)bitrate
|
||||
framerate:(NSInteger)framerate
|
||||
height:(NSInteger)height
|
||||
width:(NSInteger)width
|
||||
@@ -71,25 +72,32 @@
|
||||
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
|
||||
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
|
||||
gyroSensitivity:(CGFloat)gyroSensitivity
|
||||
localVolume:(CGFloat)localVolume
|
||||
micVolume:(CGFloat)micVolume
|
||||
touchMoveEventInterval:(NSInteger)touchMoveEventInterval
|
||||
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
|
||||
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
|
||||
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
|
||||
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
|
||||
showKeyboardToolbar:(BOOL)showKeyboardToolbar
|
||||
optimizeGames:(BOOL)optimizeGames
|
||||
multiController:(BOOL)multiController
|
||||
buttonVisualFeedback:(BOOL)buttonVisualFeedback
|
||||
touchPointTracking:(BOOL)touchPointTracking
|
||||
swapABXYButtons:(BOOL)swapABXYButtons
|
||||
audioOnPC:(BOOL)audioOnPC
|
||||
redirectMic:(BOOL)redirectMic
|
||||
useBuiltinMic:(BOOL)useBuiltinMic
|
||||
preferredCodec:(uint32_t)preferredCodec
|
||||
enableYUV444:(BOOL)enableYUV444
|
||||
enableYUV444:(BOOL)enableYUV444
|
||||
enablePIP:(BOOL)enablePIP
|
||||
fullColorRange:(BOOL)fullColorRange
|
||||
enableHdr:(BOOL)enableHdr
|
||||
btMouseSupport:(BOOL)btMouseSupport
|
||||
// absoluteTouchMode:(BOOL)absoluteTouchMode
|
||||
touchMode:(NSInteger)touchMode
|
||||
statsOverlayLevel:(NSInteger)statsOverlayLevel
|
||||
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
||||
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
||||
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
||||
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
||||
resolutionSelected:(NSInteger)resolutionSelected
|
||||
externalDisplayMode:(NSInteger)externalDisplayMode
|
||||
localMousePointerMode:(NSInteger)localMousePointerMode
|
||||
@@ -97,11 +105,36 @@
|
||||
enableGraphs:(BOOL)enableGraphs
|
||||
graphOpacity:(NSInteger)graphOpacity
|
||||
renderingBackend:(NSInteger)renderingBackend
|
||||
framePacingMode:(NSInteger)framePacingMode
|
||||
framePacingMode:(NSInteger)framePacingMode
|
||||
sendDummyEvent:(BOOL)sendDummyEvent
|
||||
rememberFoldState:(BOOL)rememberFoldState
|
||||
singleTapSensitivy:(CGFloat)singleTapSensitivy
|
||||
hapticEngine:(NSInteger)hapticEngine
|
||||
edgeSlidingSensitivity:(CGFloat)edgeSlidingSensitivity
|
||||
audioEngine:(NSInteger)audioEngine
|
||||
delayLeftClick:(BOOL)delayLeftClick
|
||||
duckOtherApps:(BOOL)duckOtherApps
|
||||
muteInBackground:(BOOL)muteInBackground
|
||||
relativeTouchSlideThreshold:(CGFloat)relativeTouchSlideThreshold
|
||||
enablePinch:(BOOL)enablePinch
|
||||
scrollSensitivity:(CGFloat)scrollSensitivity
|
||||
pinchSensitivity:(CGFloat)pinchSensitivity
|
||||
ctrlDownForPinch:(BOOL)ctrlDownForPinch
|
||||
leftClickDelayMs:(CGFloat)leftClickDelayMs
|
||||
settingsMenuOffset:(CGFloat)settingsMenuOffset
|
||||
passthroughGestures:(BOOL)passthroughGestures
|
||||
mapControllerToMouse:(BOOL)mapControllerToMouse
|
||||
controllerMousePointerVelocity:(CGFloat)controllerMousePointerVelocity
|
||||
controllerMouseExpo:(CGFloat)controllerMouseExpo
|
||||
controllerGyroSwitchMode:(NSInteger)controllerGyroSwitchMode
|
||||
enableFrameTimebase:(BOOL)enableFrameTimebase
|
||||
asyncFrameDequeue:(BOOL)asyncFrameDequeue
|
||||
sdrPerformanceWorkaround:(BOOL)sdrPerformanceWorkaround
|
||||
backgroundSessionTimer:(NSInteger)backgroundSessionTimer{
|
||||
|
||||
__block Settings* settingsToSave = settings;
|
||||
[_managedObjectContext performBlockAndWait:^{
|
||||
Settings* settingsToSave = [self retrieveSettings];
|
||||
if(!settingsToSave) settingsToSave = [self retrieveSettings];
|
||||
settingsToSave.framerate = [NSNumber numberWithInteger:framerate];
|
||||
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
|
||||
settingsToSave.height = [NSNumber numberWithInteger:height];
|
||||
@@ -118,6 +151,8 @@
|
||||
settingsToSave.touchPointerVelocityFactor = [NSNumber numberWithFloat:touchPointerVelocityFactor];
|
||||
settingsToSave.mousePointerVelocityFactor = [NSNumber numberWithFloat:mousePointerVelocityFactor];
|
||||
settingsToSave.gyroSensitivity = [NSNumber numberWithFloat:gyroSensitivity];
|
||||
settingsToSave.localVolume = [NSNumber numberWithFloat:localVolume];
|
||||
settingsToSave.micVolume = [NSNumber numberWithFloat:micVolume];
|
||||
settingsToSave.touchMoveEventInterval = [NSNumber numberWithInteger:touchMoveEventInterval];
|
||||
settingsToSave.reverseMouseWheelDirection = reverseMouseWheelDirection;
|
||||
settingsToSave.asyncNativeTouchPriority = [NSNumber numberWithInteger:asyncNativeTouchPriority];
|
||||
@@ -125,11 +160,17 @@
|
||||
settingsToSave.showKeyboardToolbar = showKeyboardToolbar;
|
||||
settingsToSave.optimizeGames = optimizeGames;
|
||||
settingsToSave.multiController = multiController;
|
||||
settingsToSave.buttonVisualFeedback = buttonVisualFeedback;
|
||||
settingsToSave.touchPointTracking = touchPointTracking;
|
||||
settingsToSave.swapABXYButtons = swapABXYButtons;
|
||||
settingsToSave.playAudioOnPC = audioOnPC;
|
||||
settingsToSave.redirectMic = redirectMic;
|
||||
settingsToSave.useBuiltinMic = useBuiltinMic;
|
||||
settingsToSave.preferredCodec = preferredCodec;
|
||||
settingsToSave.enableYUV444 = enableYUV444;
|
||||
settingsToSave.sdrPerformanceWorkaround = sdrPerformanceWorkaround;
|
||||
settingsToSave.enablePIP = enablePIP;
|
||||
settingsToSave.fullColorRange = fullColorRange;
|
||||
settingsToSave.enableHdr = enableHdr;
|
||||
settingsToSave.btMouseSupport = btMouseSupport;
|
||||
// settingsToSave.absoluteTouchMode = absoluteTouchMode;
|
||||
@@ -143,10 +184,32 @@
|
||||
settingsToSave.backroundSessionTimer = [NSNumber numberWithInteger:backgroundSessionTimer];
|
||||
|
||||
settingsToSave.frameQueueSize = [NSNumber numberWithInteger:frameQueueSize];
|
||||
settingsToSave.enableFrameTimebase = enableFrameTimebase;
|
||||
settingsToSave.asyncFrameDequeue = asyncFrameDequeue;
|
||||
settingsToSave.enableGraphs = enableGraphs;
|
||||
settingsToSave.graphOpacity = [NSNumber numberWithInteger:graphOpacity];
|
||||
settingsToSave.renderingBackend = [NSNumber numberWithInteger:renderingBackend];
|
||||
settingsToSave.framePacingMode = [NSNumber numberWithInteger:framePacingMode];
|
||||
settingsToSave.sendDummyEvent = sendDummyEvent;
|
||||
settingsToSave.singleTapSensitivity = [NSNumber numberWithDouble:singleTapSensitivy];
|
||||
settingsToSave.hapticEngine = [NSNumber numberWithInteger:hapticEngine];
|
||||
settingsToSave.edgeSlidingSensitivity = [NSNumber numberWithFloat:edgeSlidingSensitivity];
|
||||
settingsToSave.audioEngine = [NSNumber numberWithInteger:audioEngine];
|
||||
settingsToSave.delayLeftClick = delayLeftClick;
|
||||
settingsToSave.duckOtherApps = duckOtherApps;
|
||||
settingsToSave.muteInBackground = muteInBackground;
|
||||
settingsToSave.relativeTouchSlideThreshold = [NSNumber numberWithFloat:relativeTouchSlideThreshold];
|
||||
settingsToSave.enablePinch = enablePinch;
|
||||
settingsToSave.scrollSensitivity = [NSNumber numberWithFloat:scrollSensitivity];
|
||||
settingsToSave.pinchSensitivity = [NSNumber numberWithFloat:pinchSensitivity];
|
||||
settingsToSave.ctrlDownForPinch = ctrlDownForPinch;
|
||||
settingsToSave.leftClickDelayMs = [NSNumber numberWithFloat:leftClickDelayMs];
|
||||
settingsToSave.settingsMenuOffset = [NSNumber numberWithFloat:settingsMenuOffset];
|
||||
settingsToSave.passthroughGestures = passthroughGestures;
|
||||
settingsToSave.mapControllerToMouse = mapControllerToMouse;
|
||||
settingsToSave.controllerMousePointerVelocity = [NSNumber numberWithFloat:controllerMousePointerVelocity];
|
||||
settingsToSave.controllerMouseExpo = [NSNumber numberWithFloat:controllerMouseExpo];
|
||||
settingsToSave.rememberFoldState = rememberFoldState;
|
||||
[self saveData];
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property (atomic, retain) NSString *name;
|
||||
@property (atomic, retain) NSString *uuid;
|
||||
@property (atomic, retain) NSSet *appList;
|
||||
@property (atomic, retain) NSMutableSet * activeAddressPool;
|
||||
|
||||
- (id) initFromHost:(Host*)host;
|
||||
|
||||
|
||||
@@ -28,6 +28,17 @@
|
||||
self = [self init];
|
||||
|
||||
self.address = host.address;
|
||||
|
||||
NSSet *classes = [NSSet setWithObjects: [NSMutableSet class], [NSString class], nil];
|
||||
NSError *error = nil;
|
||||
self.activeAddressPool = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:host.activeAddressPool error:&error];
|
||||
if(self.activeAddressPool){
|
||||
for(NSString* address in _activeAddressPool){
|
||||
NSLog(@"%@, persisted addr: %@", host.name, address);
|
||||
}
|
||||
}
|
||||
else self.activeAddressPool = [[NSMutableSet alloc] init];
|
||||
|
||||
self.externalAddress = host.externalAddress;
|
||||
self.localAddress = host.localAddress;
|
||||
self.ipv6Address = host.ipv6Address;
|
||||
@@ -57,7 +68,7 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) propagateChangesToParent:(Host*)parentHost {
|
||||
- (void) propagateChangesToParent:(Host*)parentHost { // update persisted host data
|
||||
// Avoid overwriting existing data with nil if
|
||||
// we don't have everything populated in the temporary
|
||||
// host.
|
||||
@@ -73,6 +84,15 @@
|
||||
if (self.ipv6Address != nil) {
|
||||
parentHost.ipv6Address = self.ipv6Address;
|
||||
}
|
||||
|
||||
if(self.activeAddressPool.count>0){
|
||||
NSError *error;
|
||||
parentHost.activeAddressPool = [NSKeyedArchiver archivedDataWithRootObject:self.activeAddressPool requiringSecureCoding:YES error:&error];
|
||||
}
|
||||
else [self.activeAddressPool addObject:self.activeAddress];
|
||||
|
||||
// NSLog(@"Persisting activeAddressPool, pool count %lu ... %f, host: %@", (unsigned long)self.activeAddressPool.count, CACurrentMediaTime(), self.name);
|
||||
|
||||
// try to fix invalid mac happens in some cases
|
||||
// 添加主机crash问题重点关注
|
||||
if (!(self.mac == nil || [self.mac isEqualToString:@"00:00:00:00:00:00"])) parentHost.mac = self.mac;
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
@property (nonatomic, retain) NSNumber * touchMoveEventInterval;
|
||||
@property (nonatomic, retain) NSNumber * touchPointerVelocityFactor;
|
||||
@property (nonatomic, retain) NSNumber * gyroSensitivity;
|
||||
@property (nonatomic, retain) NSNumber * localVolume;
|
||||
@property (nonatomic, retain) NSNumber * micVolume;
|
||||
@property (nonatomic, retain) NSNumber * emulatedControllerType;
|
||||
@property (nonatomic, retain) NSNumber * mousePointerVelocityFactor;
|
||||
@property (nonatomic, retain) NSNumber * pointerVelocityModeDivider;
|
||||
@@ -40,6 +42,9 @@
|
||||
@property (nonatomic, retain) NSNumber * resolutionSelected;
|
||||
@property (nonatomic, retain) NSNumber * externalDisplayMode;
|
||||
@property (nonatomic, retain) NSNumber * localMousePointerMode;
|
||||
@property (nonatomic, retain) NSNumber * gyroBiasX;
|
||||
@property (nonatomic, retain) NSNumber * gyroBiasY;
|
||||
@property (nonatomic, retain) NSNumber * gyroBiasZ;
|
||||
@property (nonatomic, retain) NSNumber * backgroundSessionTimer;
|
||||
@property (nonatomic) enum {
|
||||
CODEC_PREF_AUTO,
|
||||
@@ -48,12 +53,18 @@
|
||||
CODEC_PREF_AV1,
|
||||
} preferredCodec;
|
||||
@property (nonatomic) BOOL enableYUV444;
|
||||
@property (nonatomic) BOOL sdrPerformanceWorkaround;
|
||||
@property (nonatomic) BOOL enablePIP;
|
||||
@property (nonatomic) BOOL fullColorRange;
|
||||
@property (nonatomic) BOOL reverseMouseWheelDirection;
|
||||
@property (nonatomic, retain) NSNumber * asyncNativeTouchPriority;
|
||||
@property (nonatomic) BOOL multiController;
|
||||
@property (nonatomic) BOOL buttonVisualFeedback;
|
||||
@property (nonatomic) BOOL touchPointTracking;
|
||||
@property (nonatomic) BOOL swapABXYButtons;
|
||||
@property (nonatomic) BOOL playAudioOnPC;
|
||||
@property (nonatomic) BOOL redirectMic;
|
||||
@property (nonatomic) BOOL useBuiltinMic;
|
||||
@property (nonatomic) BOOL optimizeGames;
|
||||
@property (nonatomic) BOOL enableHdr;
|
||||
@property (nonatomic) BOOL btMouseSupport;
|
||||
@@ -65,10 +76,43 @@
|
||||
@property (nonatomic) BOOL showKeyboardToolbar;
|
||||
@property (nonatomic) BOOL unlockDisplayOrientation;
|
||||
@property (nonatomic) BOOL enableGraphs;
|
||||
@property (nonatomic) BOOL enableFrameTimebase;
|
||||
@property (nonatomic) BOOL asyncFrameDequeue;
|
||||
@property (nonatomic, retain) NSNumber * frameQueueSize;
|
||||
@property (nonatomic, retain) NSNumber * graphOpacity;
|
||||
@property (nonatomic, retain) NSNumber * renderingBackend;
|
||||
@property (nonatomic) BOOL sendDummyEvent;
|
||||
@property (nonatomic) BOOL rememberFoldState;
|
||||
@property (nonatomic, retain) NSNumber * singleTapSensitivity;
|
||||
@property (nonatomic, retain) NSNumber * hapticEngine;
|
||||
@property (nonatomic, retain) NSNumber * appTheme;
|
||||
@property (nonatomic, retain) NSNumber * edgeSlidingSensitivity;
|
||||
@property (nonatomic, retain) NSNumber * audioEngine;
|
||||
@property (nonatomic) BOOL delayLeftClick;
|
||||
@property (nonatomic) BOOL duckOtherApps;
|
||||
@property (nonatomic) BOOL muteInBackground;
|
||||
@property (nonatomic) BOOL enablePinch;
|
||||
@property (nonatomic) BOOL ctrlDownForPinch;
|
||||
@property (nonatomic) BOOL passthroughGestures;
|
||||
@property (nonatomic, retain) NSNumber * framePacingMode;
|
||||
@property (nonatomic, retain) NSNumber * scrollSensitivity;
|
||||
@property (nonatomic, retain) NSNumber * pinchSensitivity;
|
||||
@property (nonatomic, retain) NSNumber * relativeTouchSlideThreshold;
|
||||
@property (nonatomic, retain) NSNumber * leftClickDelayMs;
|
||||
@property (nonatomic, retain) NSNumber * settingsMenuOffset;
|
||||
@property (nonatomic, retain) NSNumber * controllerMouseLeftButton;
|
||||
@property (nonatomic, retain) NSNumber * controllerMouseRightButton;
|
||||
@property (nonatomic, retain) NSNumber * controllerMouseStick;
|
||||
@property (nonatomic, retain) NSNumber * controllerMouseSwitch;
|
||||
@property (nonatomic) BOOL mapControllerToMouse;
|
||||
@property (nonatomic, retain) NSNumber * controllerMousePointerVelocity;
|
||||
@property (nonatomic, retain) NSNumber * controllerMouseExpo;
|
||||
|
||||
|
||||
// Pencil settings:
|
||||
@property (nonatomic, retain) NSNumber * pencilTickMode;
|
||||
@property (nonatomic, retain) NSNumber * pencilTickIntervalUs;
|
||||
|
||||
|
||||
- (id) initFromSettings:(Settings*)settings;
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"];
|
||||
self.enableYUV444 = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableYUV444"];
|
||||
self.enablePIP = [[NSUserDefaults standardUserDefaults] boolForKey:@"enablePIP"];
|
||||
self.fullRange = [[NSUserDefaults standardUserDefaults] boolForKey:@"fullRange"];
|
||||
self.frameQueueSize = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"frameQueueSize"]];
|
||||
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
|
||||
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
|
||||
@@ -52,15 +53,7 @@
|
||||
self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
|
||||
self.enableGraphs = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableGraphs"];
|
||||
self.graphOpacity = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"graphOpacity"]];
|
||||
// Set rendering backend based on iOS version and user preference
|
||||
NSInteger savedBackend = [[NSUserDefaults standardUserDefaults] integerForKey:@"renderingBackend"];
|
||||
if (@available(iOS 17.0, *)) {
|
||||
// iOS 17+ can use Performance mode (Metal renderer)
|
||||
self.renderingBackend = [NSNumber numberWithInteger:savedBackend];
|
||||
} else {
|
||||
// iOS < 17 must use Balanced mode (AVSB renderer)
|
||||
self.renderingBackend = [NSNumber numberWithInteger:RENDER_AVSB];
|
||||
}
|
||||
self.renderingBackend = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"renderingBackend"]];
|
||||
self.framePacingMode = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framePacingMode"]];
|
||||
|
||||
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
|
||||
@@ -95,12 +88,20 @@
|
||||
self.audioConfig = settings.audioConfig;
|
||||
self.preferredCodec = settings.preferredCodec;
|
||||
self.enableYUV444 = settings.enableYUV444;
|
||||
self.sdrPerformanceWorkaround = settings.sdrPerformanceWorkaround;
|
||||
self.enablePIP = settings.enablePIP;
|
||||
self.fullColorRange = settings.fullColorRange;
|
||||
self.frameQueueSize = settings.frameQueueSize;
|
||||
self.enableFrameTimebase = settings.enableFrameTimebase;
|
||||
self.asyncFrameDequeue = settings.asyncFrameDequeue;
|
||||
self.playAudioOnPC = settings.playAudioOnPC;
|
||||
self.redirectMic = settings.redirectMic;
|
||||
self.useBuiltinMic = settings.useBuiltinMic;
|
||||
self.enableHdr = settings.enableHdr;
|
||||
self.optimizeGames = settings.optimizeGames;
|
||||
self.multiController = settings.multiController;
|
||||
self.buttonVisualFeedback = settings.buttonVisualFeedback;
|
||||
self.touchPointTracking = settings.touchPointTracking;
|
||||
self.swapABXYButtons = settings.swapABXYButtons;
|
||||
self.onscreenControls = settings.onscreenControls;
|
||||
self.gyroMode = settings.gyroMode;
|
||||
@@ -122,6 +123,8 @@
|
||||
self.touchPointerVelocityFactor = settings.touchPointerVelocityFactor;
|
||||
self.mousePointerVelocityFactor = settings.mousePointerVelocityFactor;
|
||||
self.gyroSensitivity = settings.gyroSensitivity;
|
||||
self.localVolume = settings.localVolume;
|
||||
self.micVolume = settings.micVolume;
|
||||
self.pointerVelocityModeDivider = settings.pointerVelocityModeDivider;
|
||||
self.unlockDisplayOrientation = settings.unlockDisplayOrientation;
|
||||
self.resolutionSelected = settings.resolutionSelected;
|
||||
@@ -131,9 +134,43 @@
|
||||
self.graphOpacity = settings.graphOpacity;
|
||||
self.renderingBackend = settings.renderingBackend;
|
||||
self.framePacingMode = settings.framePacingMode;
|
||||
self.sendDummyEvent = settings.sendDummyEvent;
|
||||
self.rememberFoldState = settings.rememberFoldState;
|
||||
self.gyroBiasX = settings.gyroBiasX;
|
||||
self.gyroBiasY = settings.gyroBiasY;
|
||||
self.gyroBiasZ = settings.gyroBiasZ;
|
||||
self.singleTapSensitivity = settings.singleTapSensitivity;
|
||||
self.backgroundSessionTimer = settings.backroundSessionTimer;
|
||||
#endif
|
||||
self.edgeSlidingSensitivity = settings.edgeSlidingSensitivity;
|
||||
self.appTheme = settings.appTheme;
|
||||
self.hapticEngine = settings.hapticEngine;
|
||||
self.uniqueId = settings.uniqueId;
|
||||
self.audioEngine = settings.audioEngine;
|
||||
self.delayLeftClick = settings.delayLeftClick;
|
||||
self.duckOtherApps = settings.duckOtherApps;
|
||||
self.muteInBackground = settings.muteInBackground;
|
||||
self.relativeTouchSlideThreshold = settings.relativeTouchSlideThreshold;
|
||||
self.enablePinch = settings.enablePinch;
|
||||
self.scrollSensitivity = settings.scrollSensitivity;
|
||||
self.pinchSensitivity = settings.pinchSensitivity;
|
||||
self.leftClickDelayMs = settings.leftClickDelayMs;
|
||||
self.ctrlDownForPinch = settings.ctrlDownForPinch;
|
||||
self.settingsMenuOffset = settings.settingsMenuOffset;
|
||||
self.passthroughGestures = settings.passthroughGestures;
|
||||
self.mapControllerToMouse = settings.mapControllerToMouse;
|
||||
self.controllerMouseLeftButton = settings.controllerMouseLeftButton;
|
||||
self.controllerMouseRightButton = settings.controllerMouseRightButton;
|
||||
self.controllerMouseSwitch = settings.controllerMouseSwitch;
|
||||
self.controllerMouseStick = settings.controllerMouseStick;
|
||||
self.controllerMousePointerVelocity = settings.controllerMousePointerVelocity;
|
||||
self.controllerMouseExpo = settings.controllerMouseExpo;
|
||||
|
||||
|
||||
// Pencil settings:
|
||||
self.pencilTickMode = settings.pencilTickMode;
|
||||
self.pencilTickIntervalUs = settings.pencilTickIntervalUs;
|
||||
|
||||
#endif
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,6 @@
|
||||
- (void)resizeBySizeFactor:(CGFloat)factor;
|
||||
- (id) initWithHost:(TemporaryHost*)host;
|
||||
- (id) initWithHost:(TemporaryHost*)host andSizeFactor:(CGFloat)sizeFactor;
|
||||
- (void)updateTheme;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#import "HostCardView.h"
|
||||
#import "LocalizationHelper.h"
|
||||
#import "ThemeManager.h"
|
||||
#import "VoidLink-Swift.h"
|
||||
#import "FixedTintImageView.h"
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
computerIconMonitorCenterYOffset = [self isIPhone] ? -2.75*_sizeFactor : -3.2*_sizeFactor;
|
||||
iconAndButtonSpacing = 37*_sizeFactor;
|
||||
buttonHeight = 39*_sizeFactor;
|
||||
defaultBlue = [ThemeManager appPrimaryColor];
|
||||
defaultBlue = ThemeManager.appPrimaryColor;
|
||||
defaultGreen = [UIColor colorWithRed:52.0/255.0 green:199.0/255.0 blue:89.0/255.0 alpha:1.0];
|
||||
// self.userIterfaceStyle = UIUserInterfaceStyleLight
|
||||
|
||||
@@ -165,7 +165,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
|
||||
- (void)setupUI {
|
||||
self.userInteractionEnabled = YES;
|
||||
self.backgroundColor = [ThemeManager widgetBackgroundColor]; // theme
|
||||
self.backgroundColor = ThemeManager.widgetBackgroundColor; // theme
|
||||
self.cardContentpadding = 13 * _sizeFactor;
|
||||
self.clipsToBounds = YES;
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
@@ -296,7 +296,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
self.appButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.appButton.frame = CGRectMake(20, 200, 150, buttonHeight);
|
||||
[self.appButton setTitle:[LocalizationHelper localizedStringForKey:@"Applications"] forState:UIControlStateNormal];
|
||||
[self.appButton setTitleColor:[ThemeManager textColorGray] forState:UIControlStateNormal]; //theme
|
||||
[self.appButton setTitleColor:ThemeManager.textColorGray forState:UIControlStateNormal]; //theme
|
||||
self.appButton.titleLabel.font = [UIFont systemFontOfSize:buttonLabelFontSize];
|
||||
[self.appButton addTarget:self action:@selector(appButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
[self addSubview:self.appButton];
|
||||
@@ -325,7 +325,6 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight*0.263];
|
||||
[self.launchButton setImage:[UIImage systemImageNamed:@"play.fill" withConfiguration:config] forState:UIControlStateNormal];
|
||||
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
@@ -343,7 +342,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
self.pairButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
self.pairButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.pairButton.frame = CGRectMake(0, 0, 150, 50);
|
||||
self.pairButton.backgroundColor = [ThemeManager textTintColorWithAlpha];
|
||||
self.pairButton.backgroundColor = ThemeManager.textTintColorWithAlpha;
|
||||
self.pairButton.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0377/2);
|
||||
[self.pairButton setTitle:[LocalizationHelper localizedStringForKey:@" Pair with PIN"] forState:UIControlStateNormal];
|
||||
[self.pairButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
||||
@@ -356,7 +355,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
self.pairButton.backgroundColor = [ThemeManager textTintColorWithAlpha];
|
||||
self.pairButton.backgroundColor = ThemeManager.textTintColorWithAlpha;
|
||||
[self.pairButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
||||
|
||||
[self.pairButton addTarget:self action:@selector(pairButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
@@ -377,7 +376,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
self.wakeupButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
self.wakeupButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.wakeupButton.frame = CGRectMake(0, 0, 150, 50);
|
||||
self.wakeupButton.backgroundColor = [ThemeManager textTintColorWithAlpha];
|
||||
self.wakeupButton.backgroundColor = ThemeManager.textTintColorWithAlpha;
|
||||
self.wakeupButton.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0377/2);
|
||||
[self.wakeupButton setTitle:[LocalizationHelper localizedStringForKey:@" Wake-on-LAN"] forState:UIControlStateNormal];
|
||||
[self.wakeupButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
||||
@@ -425,17 +424,15 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
[self.transparentButton.bottomAnchor constraintEqualToAnchor:self.separatorLine.topAnchor],
|
||||
]];
|
||||
|
||||
|
||||
_widthConstraint.constant = _cardContentpadding*2 + appButtonWidth + 15*_sizeFactor + launchButtonWidth;
|
||||
_heightConstraint.constant = _cardContentpadding*2 + _iconBackgroundView.frame.size.height + iconAndButtonSpacing + buttonHeight + 1;
|
||||
|
||||
_size = CGSizeMake(_widthConstraint.constant, _heightConstraint.constant);
|
||||
|
||||
[self updateTheme:ThemeManager.userInterfaceStyle];
|
||||
[self updateTheme];
|
||||
}
|
||||
|
||||
- (void)createBackgroundLayer{
|
||||
backgroundLayer = [CAGradientLayer layer];
|
||||
- (void)updateBackgroundLayerTheme{
|
||||
UIColor *gradientColorDark = [UIColor colorWithRed:0.0 green:0.319 blue:0.64 alpha:1.0];
|
||||
UIColor *gradientColorLight = [gradientColorDark colorWithAlphaComponent:0.52];
|
||||
CGColorRef gradientColorRef = ThemeManager.userInterfaceStyle == UIUserInterfaceStyleDark ? gradientColorDark.CGColor : gradientColorLight.CGColor;
|
||||
@@ -445,7 +442,11 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
(__bridge id)[UIColor clearColor].CGColor,
|
||||
(__bridge id)gradientColorRef
|
||||
];
|
||||
}
|
||||
|
||||
- (void)createBackgroundLayer{
|
||||
backgroundLayer = [CAGradientLayer layer];
|
||||
[self updateBackgroundLayerTheme];
|
||||
backgroundLayer.locations = @[@0, @0.18, @0.5, @1];
|
||||
backgroundLayer.startPoint = CGPointMake(0.25, 0.5);
|
||||
backgroundLayer.endPoint = CGPointMake(0.75, 0.5);
|
||||
@@ -469,12 +470,15 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
}
|
||||
|
||||
|
||||
- (void)updateTheme:(UIUserInterfaceStyle)userIterfaceStyle{
|
||||
self.backgroundColor = [ThemeManager widgetBackgroundColor];
|
||||
_hostNameLabel.textColor = [ThemeManager textColor];
|
||||
[_appButton setTitleColor:[ThemeManager appPrimaryColor] forState:UIControlStateNormal];
|
||||
_separatorLine.backgroundColor = [ThemeManager separatorColor];
|
||||
- (void)updateTheme {
|
||||
self.backgroundColor = ThemeManager.widgetBackgroundColor;
|
||||
_hostNameLabel.textColor = ThemeManager.textColor;
|
||||
[_appButton setTitleColor:ThemeManager.appPrimaryColor forState:UIControlStateNormal];
|
||||
_separatorLine.backgroundColor = ThemeManager.hostCardSeparatorColor;
|
||||
|
||||
backgroundLayer.hidden = NO;
|
||||
[self updateBackgroundLayerTheme];
|
||||
|
||||
[self updateContentsForHost:_host];
|
||||
}
|
||||
|
||||
@@ -500,7 +504,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
}
|
||||
|
||||
- (bool)isDarkTheme{
|
||||
return self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark;
|
||||
return ThemeManager.userInterfaceStyle == UIUserInterfaceStyleDark;
|
||||
}
|
||||
|
||||
- (void) updateLoop {
|
||||
@@ -555,7 +559,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
[_launchButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // theme
|
||||
}
|
||||
else {
|
||||
_iconBackgroundView.backgroundColor = [ThemeManager appPrimaryColorWithAlpha];
|
||||
_iconBackgroundView.backgroundColor = ThemeManager.appPrimaryColorWithAlpha;
|
||||
UIColor* iconColor = [[UIColor whiteColor] colorWithAlphaComponent:[self isDarkTheme] ? 0.63 : 1];
|
||||
_hostIconView.tintColor = iconColor;
|
||||
lockIconView.tintColor = iconColor;
|
||||
@@ -569,18 +573,18 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
case StateOffline:
|
||||
[_hostSpinner stopAnimating];
|
||||
// _iconBackgroundView.backgroundColor = _userIterfaceStyle == UIUserInterfaceStyleDark ? [UIColor appBackgroundColorDark] : [UIColor appBackgroundColorLight];
|
||||
_iconBackgroundView.backgroundColor = [ThemeManager appBackgroundColor];
|
||||
_statusLabel.textColor = [ThemeManager textColorGray];
|
||||
_iconBackgroundView.backgroundColor = ThemeManager.offlineHostIconBackgroundColor;
|
||||
_statusLabel.textColor = ThemeManager.textColorGray;
|
||||
_statusLabel.text = [LocalizationHelper localizedStringForKey: @"Offline"];
|
||||
_statusIcon.tintColor = [ThemeManager textColorGray];
|
||||
_statusIcon.tintColor = ThemeManager.textColorGray;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
_statusIcon.image = [UIImage systemImageNamed:@"exclamationmark.triangle.fill"];
|
||||
} else {
|
||||
self.statusIcon.hidden = YES;
|
||||
}
|
||||
|
||||
_hostIconView.tintColor = [ThemeManager lowProfileGray];
|
||||
lockIconView.tintColor = [ThemeManager lowProfileGray];
|
||||
_hostIconView.tintColor = ThemeManager.lowProfileGray;
|
||||
lockIconView.tintColor = ThemeManager.lowProfileGray;
|
||||
lockIconView.hidden = hostPaired;
|
||||
_appButton.hidden = YES;
|
||||
_launchButton.hidden = YES;
|
||||
@@ -590,24 +594,24 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.45 weight:UIImageSymbolWeightBold];
|
||||
UIImage *templateImage = [UIImage systemImageNamed:@"power" withConfiguration:config];
|
||||
UIImage *coloredImage = [templateImage imageWithTintColor:hasValidMac ? defaultBlue : [ThemeManager textColorGray] renderingMode:UIImageRenderingModeAlwaysOriginal];
|
||||
UIImage *coloredImage = [templateImage imageWithTintColor:hasValidMac ? defaultBlue : ThemeManager.textColorGray renderingMode:UIImageRenderingModeAlwaysOriginal];
|
||||
[self.wakeupButton setImage:coloredImage forState:UIControlStateNormal];
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
|
||||
self.wakeupButton.backgroundColor = hasValidMac ? [ThemeManager textTintColorWithAlpha] : [[ThemeManager textColorGray] colorWithAlphaComponent:0.2];
|
||||
[self.wakeupButton setTitleColor: hasValidMac ? defaultBlue : [ThemeManager textColorGray] forState:UIControlStateNormal];
|
||||
self.wakeupButton.backgroundColor = hasValidMac ? ThemeManager.textTintColorWithAlpha : [ThemeManager.textColorGray colorWithAlphaComponent:0.2];
|
||||
[self.wakeupButton setTitleColor: hasValidMac ? defaultBlue : ThemeManager.textColorGray forState:UIControlStateNormal];
|
||||
|
||||
break;
|
||||
case StateUnknown:
|
||||
_hostSpinner.color = [UIColor whiteColor];
|
||||
[_hostSpinner startAnimating];
|
||||
// _iconBackgroundView.backgroundColor = _userIterfaceStyle == UIUserInterfaceStyleDark ? [UIColor appBackgroundColorDark] : [UIColor appBackgroundColorLight];
|
||||
_iconBackgroundView.backgroundColor = [ThemeManager appPrimaryColorWithAlpha];
|
||||
_statusLabel.textColor = [ThemeManager textColorGray];
|
||||
_iconBackgroundView.backgroundColor = ThemeManager.appPrimaryColorWithAlpha;
|
||||
_statusLabel.textColor = ThemeManager.textColorGray;
|
||||
_statusLabel.text = [LocalizationHelper localizedStringForKey: @"Detecting..."];
|
||||
_statusIcon.tintColor = [ThemeManager textColorGray];
|
||||
_statusIcon.tintColor = ThemeManager.textColorGray;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
_statusIcon.image = [UIImage systemImageNamed:@"antenna.radiowaves.left.and.right"];
|
||||
} else {
|
||||
@@ -624,13 +628,13 @@ static const float REFRESH_CYCLE = 2.0f;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.45 weight:UIImageSymbolWeightBold];
|
||||
UIImage *templateImage = [UIImage systemImageNamed:@"power" withConfiguration:config];
|
||||
UIImage *coloredImage = [templateImage imageWithTintColor:hasValidMac ? defaultBlue : [ThemeManager textColorGray] renderingMode:UIImageRenderingModeAlwaysOriginal];
|
||||
UIImage *coloredImage = [templateImage imageWithTintColor:hasValidMac ? defaultBlue : ThemeManager.textColorGray renderingMode:UIImageRenderingModeAlwaysOriginal];
|
||||
[self.wakeupButton setImage:coloredImage forState:UIControlStateNormal];
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
self.wakeupButton.backgroundColor = hasValidMac ? [ThemeManager textTintColorWithAlpha] : [[ThemeManager textColorGray] colorWithAlphaComponent:0.2];
|
||||
[self.wakeupButton setTitleColor: hasValidMac ? defaultBlue : [ThemeManager textColorGray] forState:UIControlStateNormal];
|
||||
self.wakeupButton.backgroundColor = hasValidMac ? ThemeManager.textTintColorWithAlpha : [ThemeManager.textColorGray colorWithAlphaComponent:0.2];
|
||||
[self.wakeupButton setTitleColor: hasValidMac ? defaultBlue : ThemeManager.textColorGray forState:UIControlStateNormal];
|
||||
break;
|
||||
default:break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "StickWheel.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" fill="none" viewBox="0 0 120 120">
|
||||
<g clip-path="url(#a)">
|
||||
<g filter="url(#b)">
|
||||
<path fill="#fff" fill-opacity="1"
|
||||
d="M67.25 10.437c.099.075.215.123.338.142a50 50 0 0 1 27.233 13.54.74.74 0 0 1 .004 1.056l-.776.775a.76.76 0 0 1-1.065.004 47.405 47.405 0 0 0-65.97 0 .76.76 0 0 1-1.064-.004l-.776-.775a.74.74 0 0 1 .004-1.057A50 50 0 0 1 52.41 10.58a.76.76 0 0 0 .339-.142l6.8-5.1a.75.75 0 0 1 .9 0z"/>
|
||||
</g>
|
||||
<g filter="url(#c)">
|
||||
<path fill="#fff" fill-opacity="1"
|
||||
d="M61.955 6.804c2.487 2.295 5.657 3.663 8.96 4.402a50 50 0 0 1 23.51 12.533c.52.493.52 1.316.013 1.823a1.326 1.326 0 0 1-1.849.013 47.403 47.403 0 0 0-65.179 0 1.326 1.326 0 0 1-1.848-.013 1.27 1.27 0 0 1 .012-1.823 50 50 0 0 1 23.512-12.533c3.302-.739 6.47-2.106 8.958-4.401L60 5z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<!-- defs stay the same -->
|
||||
<filter id="b" width="82.089" height="32.983" x="18.955" y="-.813" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur result="effect1_foregroundBlur_211_1847" stdDeviation="3"/>
|
||||
</filter>
|
||||
<filter id="c" width="69.634" height="22.444" x="25.183" y="5" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"/>
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_211_1847"/>
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_211_1847" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h120v120H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "gyroscope.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 341-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 22.7531 22.4167">
|
||||
<g>
|
||||
<rect height="22.4167" opacity="0" width="22.7531" x="0" y="0"/>
|
||||
<path d="M4.66382 17.7636C6.51148 19.6044 10.4016 18.2265 14.3104 14.3211C18.2231 10.4016 19.6044 6.51148 17.7568 4.67065C15.8832 2.7976 12.0151 4.17898 8.08824 8.09898C4.17557 12.0224 2.79027 15.8832 4.66382 17.7636ZM5.99819 16.4151C4.94393 15.3574 6.33263 12.5482 9.42944 9.44409C12.5521 6.3214 15.3647 4.94734 16.4151 6.0016C17.4694 7.04854 16.0841 9.85382 12.9687 12.9833C9.86457 16.0841 7.04854 17.4728 5.99819 16.4151ZM17.7456 17.7602C19.6118 15.8832 18.2304 12.0258 14.3104 8.09507C10.4016 4.17898 6.52613 2.7976 4.64917 4.66724C2.80493 6.51148 4.19022 10.4016 8.1024 14.3138C12.0151 18.2265 15.8979 19.5971 17.7456 17.7602ZM16.4044 16.4151C15.3608 17.4728 12.5482 16.0841 9.42944 12.9687C6.31798 9.85724 4.94393 7.04854 5.99086 6.0016C7.04121 4.94734 9.85724 6.3214 12.9687 9.44018C16.0767 12.5516 17.4694 15.3608 16.4044 16.4151ZM19.0764 4.59024L21.8772 1.7895C22.2092 1.45357 22.2092 0.882298 21.8772 0.550279C21.5271 0.203607 20.97 0.210933 20.6272 0.550279L17.7762 3.39741ZM3.61603 17.5507L0.550279 20.6272C0.200191 20.9665 0.210933 21.5237 0.550279 21.863C0.878882 22.2058 1.43941 22.2097 1.7895 21.8737L5.03247 18.6342ZM11.203 22.406C17.3909 22.406 22.4094 17.3875 22.4094 11.203C22.4094 5.01849 17.3909 0 11.203 0C5.01849 0 0 5.01849 0 11.203C0 17.3875 5.01849 22.406 11.203 22.406ZM11.203 19.9444C6.36936 19.9444 2.46154 16.0366 2.46154 11.203C2.46154 6.36936 6.36936 2.46154 11.203 2.46154C16.0366 2.46154 19.9478 6.36936 19.9478 11.203C19.9478 16.0366 16.0366 19.9444 11.203 19.9444Z" fill="white" fill-opacity="0.85"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -11,8 +11,10 @@
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AbsoluteTouchHandler : UIResponder
|
||||
@property (class, nonatomic, assign) int mouseButtonForCursorMove;
|
||||
|
||||
-(id)initWithView:(StreamView*)view;
|
||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
||||
- (void)pauseLeftButtonDrag;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
//
|
||||
|
||||
#import "AbsoluteTouchHandler.h"
|
||||
#import "VoidLink-Swift.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
@@ -17,7 +18,7 @@
|
||||
#define LONG_PRESS_ACTIVATION_DELAY 0.650f
|
||||
|
||||
// How far the finger can move before it cancels a right click
|
||||
#define LONG_PRESS_ACTIVATION_DELTA 0.01f
|
||||
#define LONG_PRESS_ACTIVATION_DELTA 0.02f
|
||||
|
||||
// How long the double tap deadzone stays in effect between touch up and touch down
|
||||
#define DOUBLE_TAP_DEAD_ZONE_DELAY 0.250f
|
||||
@@ -25,8 +26,13 @@
|
||||
// How far the finger can move before it can override the double tap deadzone
|
||||
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
|
||||
|
||||
static int mouseButtonForCursorMove = BUTTON_LEFT;
|
||||
|
||||
@implementation AbsoluteTouchHandler {
|
||||
StreamView* streamView;
|
||||
__weak StreamView* streamView;
|
||||
|
||||
bool multiTouchesDetected;
|
||||
bool passthroughGestures;
|
||||
|
||||
NSTimer* longPressTimer;
|
||||
UITouch* lastTouchDown;
|
||||
@@ -34,37 +40,83 @@
|
||||
UITouch* lastTouchUp;
|
||||
CGPoint lastTouchUpLocation;
|
||||
|
||||
UITouch* capturedTouch;
|
||||
|
||||
CGPoint touchBeganLocation;
|
||||
CGPoint movingTouchLocation;
|
||||
|
||||
NSTimeInterval touchBeganTimeStamp;
|
||||
NSTimeInterval leftClickTimeThreshold;
|
||||
|
||||
// upper screen edge check
|
||||
bool touchPointSpawnedAtUpperScreenEdge;
|
||||
CGFloat slideGestureVerticalThreshold;
|
||||
CGFloat screenWidthWithThreshold;
|
||||
CGFloat EDGE_TOLERANCE;
|
||||
CGFloat _edgeTolerance;
|
||||
|
||||
bool _delayMouseLeftClick;
|
||||
NSTimeInterval leftClickDelay;
|
||||
|
||||
bool dragButtonDown;
|
||||
UInt8 currentTouchesCount;
|
||||
|
||||
bool rightButtonClicked;
|
||||
}
|
||||
|
||||
- (id)initWithView:(StreamView*)view {
|
||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings {
|
||||
self = [self init];
|
||||
self->streamView = view;
|
||||
|
||||
multiTouchesDetected = false;
|
||||
passthroughGestures = settings.passthroughGestures;
|
||||
|
||||
_delayMouseLeftClick = settings.delayLeftClick;
|
||||
// _delayMouseLeftClick = true;
|
||||
dragButtonDown = false;
|
||||
|
||||
leftClickTimeThreshold = 0.1;
|
||||
|
||||
// upper screen check
|
||||
EDGE_TOLERANCE = 10.0;
|
||||
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
|
||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
|
||||
self->touchPointSpawnedAtUpperScreenEdge = false;
|
||||
|
||||
leftClickDelay = ((CGFloat)settings.leftClickDelayMs.intValue)/1000;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)onLongPressStart:(NSTimer*)timer {
|
||||
// Raise the left click and start a right click
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
if(multiTouchesDetected) return;
|
||||
|
||||
if([self touchDidntMoveOnScreen:movingTouchLocation]){
|
||||
if(_delayMouseLeftClick){
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
if(mouseButtonForCursorMove!=BUTTON_LEFT) LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, mouseButtonForCursorMove);
|
||||
}
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
dispatch_time_t delayShort = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC));
|
||||
dispatch_after(delayShort, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
self->rightButtonClicked = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
rightButtonClicked = false;
|
||||
|
||||
if([UITouchUtil touchesIn:streamView from:event].count>=2){
|
||||
multiTouchesDetected = true;
|
||||
[longPressTimer invalidate];
|
||||
longPressTimer = nil;
|
||||
return;
|
||||
}
|
||||
|
||||
CGPoint initialPoint = [[touches anyObject] locationInView:streamView];
|
||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < _edgeTolerance || initialPoint.x > screenWidthWithThreshold)) {
|
||||
self->touchPointSpawnedAtUpperScreenEdge = true;
|
||||
return; // we're done here. this touch event will not be sent to the remote PC.
|
||||
}
|
||||
@@ -72,22 +124,27 @@
|
||||
touchPointSpawnedAtUpperScreenEdge = false; // reset this flag immediately if we get a touch event passing the check above, this fixes irresponsive touch after closing the command tool menu.
|
||||
|
||||
// Ignore touch down events with more than one finger
|
||||
/*
|
||||
if ([[event allTouches] count] > 1) {
|
||||
return;
|
||||
}
|
||||
}*/
|
||||
|
||||
UITouch* touch = [touches anyObject];
|
||||
CGPoint touchLocation = [touch locationInView:streamView];
|
||||
capturedTouch = [touches anyObject];
|
||||
CGPoint touchLocation = [capturedTouch locationInView:streamView];
|
||||
|
||||
touchBeganTimeStamp = capturedTouch.timestamp;
|
||||
|
||||
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
|
||||
if (touch.timestamp - lastTouchUp.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
|
||||
if (capturedTouch.timestamp - lastTouchUp.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
|
||||
sqrt(pow((touchLocation.x / streamView.bounds.size.width) - (lastTouchUpLocation.x / streamView.bounds.size.width), 2) +
|
||||
pow((touchLocation.y / streamView.bounds.size.height) - (lastTouchUpLocation.y / streamView.bounds.size.height), 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
|
||||
[streamView updateCursorLocation:touchLocation isMouse:NO];
|
||||
if(!multiTouchesDetected) [streamView updateCursorLocation:touchLocation isMouse:NO];
|
||||
}
|
||||
|
||||
// Press the left button down
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
if(!_delayMouseLeftClick){
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); //deprecated
|
||||
}
|
||||
|
||||
// Start the long press timer
|
||||
longPressTimer = [NSTimer scheduledTimerWithTimeInterval:LONG_PRESS_ACTIVATION_DELAY
|
||||
@@ -96,8 +153,21 @@
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
|
||||
lastTouchDown = touch;
|
||||
lastTouchDown = capturedTouch;
|
||||
lastTouchDownLocation = touchLocation;
|
||||
movingTouchLocation = touchLocation;
|
||||
touchBeganLocation = touchLocation;
|
||||
currentTouchesCount = [UITouchUtil touchesIn:streamView from:event].count;
|
||||
}
|
||||
|
||||
- (void)pauseLeftButtonDrag{
|
||||
if(dragButtonDown){
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC));
|
||||
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, mouseButtonForCursorMove);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
@@ -105,42 +175,90 @@
|
||||
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
||||
|
||||
// Ignore touch move events with more than one finger
|
||||
/*
|
||||
if ([[event allTouches] count] > 1) {
|
||||
return;
|
||||
}*/
|
||||
|
||||
NSSet* currentTouches = [UITouchUtil touchesIn:streamView from:event];
|
||||
currentTouchesCount = currentTouches.count;
|
||||
|
||||
if(currentTouchesCount == 2){
|
||||
if(mouseButtonForCursorMove!=BUTTON_LEFT) LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, mouseButtonForCursorMove);
|
||||
if(passthroughGestures) [TouchPadGestureHandler handleGestureIn:streamView with:event];
|
||||
}
|
||||
|
||||
UITouch* touch = [touches anyObject];
|
||||
CGPoint touchLocation = [touch locationInView:streamView];
|
||||
if(currentTouchesCount > 1) return;
|
||||
|
||||
if (sqrt(pow((touchLocation.x / streamView.bounds.size.width) - (lastTouchDownLocation.x / streamView.bounds.size.width), 2) +
|
||||
pow((touchLocation.y / streamView.bounds.size.height) - (lastTouchDownLocation.y / streamView.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) {
|
||||
if(![currentTouches containsObject:capturedTouch]) return;
|
||||
|
||||
movingTouchLocation = [capturedTouch locationInView:streamView];
|
||||
|
||||
if (sqrt(pow((movingTouchLocation.x / streamView.bounds.size.width) - (lastTouchDownLocation.x / streamView.bounds.size.width), 2) +
|
||||
pow((movingTouchLocation.y / streamView.bounds.size.height) - (lastTouchDownLocation.y / streamView.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) {
|
||||
// Moved too far since touch down. Cancel the long press timer.
|
||||
[longPressTimer invalidate];
|
||||
longPressTimer = nil;
|
||||
|
||||
NSTimeInterval dragDelay = mouseButtonForCursorMove == BUTTON_LEFT ? leftClickTimeThreshold : 0;
|
||||
|
||||
if(_delayMouseLeftClick && (CACurrentMediaTime()-touchBeganTimeStamp>dragDelay) && !dragButtonDown){
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, mouseButtonForCursorMove);
|
||||
dragButtonDown = true;
|
||||
}
|
||||
}
|
||||
|
||||
[streamView updateCursorLocation:[[touches anyObject] locationInView:streamView] isMouse:NO];
|
||||
if(!rightButtonClicked) [streamView updateCursorLocation:movingTouchLocation isMouse:NO];
|
||||
}
|
||||
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
[longPressTimer invalidate];
|
||||
longPressTimer = nil;
|
||||
|
||||
if(TouchPadGestureHandler.ctrlDown) LiSendKeyboardEvent(CommandManager.keyboardButtonMappings[@"CTRL"].shortValue,KEY_ACTION_UP,0);
|
||||
|
||||
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
||||
|
||||
if(multiTouchesDetected) {
|
||||
if([UITouchUtil touchesIn:streamView from:event].count == touches.count) multiTouchesDetected = false;
|
||||
return;
|
||||
};
|
||||
|
||||
if([UITouchUtil touchesIn:streamView from:event].count == touches.count) multiTouchesDetected = false;
|
||||
|
||||
// Only fire this logic if all touches have ended
|
||||
if ([[event allTouches] count] == [touches count]) {
|
||||
if ([touches containsObject:capturedTouch]) {
|
||||
// Cancel the long press timer
|
||||
[longPressTimer invalidate];
|
||||
longPressTimer = nil;
|
||||
|
||||
// Left button up on finger up
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
|
||||
// Raise right button too in case we triggered a long press gesture
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
|
||||
// Remember this last touch for touch-down deadzoning
|
||||
CGPoint touchEndLocation = [capturedTouch locationInView:streamView];
|
||||
|
||||
if(_delayMouseLeftClick){
|
||||
if(CACurrentMediaTime()-touchBeganTimeStamp<leftClickTimeThreshold) {
|
||||
if(CACurrentMediaTime()-lastTouchUp.timestamp<0.15
|
||||
&& ![self isAdjacentPoints:touchEndLocation from:lastTouchUpLocation tolerance:30]) [streamView updateCursorLocation:touchEndLocation isMouse:NO];
|
||||
if([self touchDidntMoveOnScreen:touchEndLocation] && !rightButtonClicked) [self sendShortMouseLeftButtonClickEvent];
|
||||
}
|
||||
else if(!rightButtonClicked){
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
if(mouseButtonForCursorMove!=BUTTON_LEFT) LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, mouseButtonForCursorMove);
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
}
|
||||
}
|
||||
else{
|
||||
// Left button up on finger up
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
|
||||
// Raise right button too in case we triggered a long press gesture
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
}
|
||||
|
||||
lastTouchUp = [touches anyObject];
|
||||
lastTouchUpLocation = [lastTouchUp locationInView:streamView];
|
||||
|
||||
dragButtonDown = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,4 +267,34 @@
|
||||
[self touchesEnded:touches withEvent:event];
|
||||
}
|
||||
|
||||
- (void)sendShortMouseLeftButtonClickEvent{
|
||||
dispatch_time_t delayShort = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(leftClickDelay * NSEC_PER_SEC));
|
||||
dispatch_time_t delayLong = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.03 * NSEC_PER_SEC));
|
||||
dispatch_after(delayShort, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
dispatch_after(delayLong, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (bool)touchDidntMoveOnScreen:(CGPoint)touchLocation{
|
||||
return [self isAdjacentPoints:touchLocation from:touchBeganLocation tolerance:3];
|
||||
}
|
||||
|
||||
- (BOOL)isAdjacentPoints:(CGPoint)currentPoint from:(CGPoint)originalPoint tolerance:(CGFloat)tolerance {
|
||||
bool isAdjacent = hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) <= hypot(tolerance, tolerance);
|
||||
return isAdjacent;
|
||||
}
|
||||
|
||||
+ (int)mouseButtonForCursorMove {
|
||||
return mouseButtonForCursorMove;
|
||||
}
|
||||
|
||||
+ (void)setMouseButtonForCursorMove:(int)button {
|
||||
mouseButtonForCursorMove = button;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -61,7 +61,9 @@ import UIKit
|
||||
"M_X1" : BUTTON_X1,
|
||||
"MX1" : BUTTON_X1,
|
||||
"M_X2" : BUTTON_X2,
|
||||
"MX2" : BUTTON_X2
|
||||
"MX2" : BUTTON_X2,
|
||||
"WHEELUP" : 0xFF,
|
||||
"WHEELDOWN" : -0xFF,
|
||||
]
|
||||
|
||||
|
||||
@@ -96,6 +98,7 @@ import UIKit
|
||||
"PADDLE3" : PADDLE3_FLAG,
|
||||
"PADDLE4" : PADDLE4_FLAG,
|
||||
"MISC" : MISC_FLAG,
|
||||
"OSCHOME" : SPECIAL_FLAG,
|
||||
"OSCL2" : 0,
|
||||
"L2" : 0,
|
||||
"LT" : 0,
|
||||
@@ -115,16 +118,49 @@ import UIKit
|
||||
"OSCBACK"
|
||||
]
|
||||
|
||||
@objc public static let touchPadCmds: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD", "DS4TOUCH", "MOUSEPAD", "DPAD", "TRACKBALL", "WASDPAD", "ARROWPAD"]
|
||||
@objc public static let touchPadCmds: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD", "LSWHEEL", "RSWHEEL", "LTPAD", "RTPAD", "DS4TOUCH", "MOUSEPAD", "ABSMOUSE", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL", "DPAD", "TRACKBALL", "WASDPAD", "ARROWPAD"]
|
||||
@objc public static let mousePadWithButtonActions: [String] = ["MOUSEPAD", "ABSMOUSE"]
|
||||
@objc public static let mousePad: [String] = ["MOUSEPAD", "ABSMOUSE", "TRACKBALL"]
|
||||
@objc public static let directionPads: [String] = ["DPAD", "WASDPAD", "ARROWPAD"]
|
||||
@objc public static let stickTouchPads: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD"]
|
||||
@objc public static let nonVectorStickPads: [String] = ["LSPAD", "RSPAD"]
|
||||
@objc public static let specialOverlayButtonCmds: [String] = ["SETTINGS"]
|
||||
@objc public static let stickWheels: [String] = ["LSWHEEL", "RSWHEEL"]
|
||||
@objc public static let vectorTouchPads: [String] = ["LSVPAD", "RSVPAD", "MOUSEPAD", "TRACKBALL"]
|
||||
@objc public static let inertialTouchPads: [String] = ["LSVPAD", "RSVPAD", "TRACKBALL"]
|
||||
@objc public static let verticalTouchPads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"]
|
||||
@objc public static let bidirectionalVerticalTouchPads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"]
|
||||
@objc public static let functionalButtonCmds: [String] = [
|
||||
"SETTINGS",
|
||||
"TOOLBOX",
|
||||
"WIDGETTOOL",
|
||||
"WIDGETPROFILES",
|
||||
"PROFILES",
|
||||
"PICKPROFILE",
|
||||
"PICKPRFL",
|
||||
"SOFTKEYBOARD",
|
||||
"ABSTCHDRAG",
|
||||
"FOLDER",
|
||||
"PRESSURECURVE",
|
||||
"PENCILHOVER",
|
||||
"BRUSH",
|
||||
"ERASER",
|
||||
"NOSINGLETOUCH"
|
||||
]
|
||||
@objc public static let pencilProButtonCmds: [String] = [
|
||||
"PENCILHOVER",
|
||||
"BRUSH",
|
||||
"ERASER",
|
||||
"NOSINGLETOUCH"
|
||||
]
|
||||
@objc public static let motionControlButtonCmds: [String] = ["GYRO","GYROPAUSE","ACCEL","MOTION"]
|
||||
|
||||
// @objc public static let specialGameWidgets: [String] = ["YSRSV", "YSLT", "YSRT", "YSRB", "YSB", "YSRT2", "YSRB2", "YSB2", "YSEM", "YSML", "YSMR", "YSWASD"]
|
||||
|
||||
static let keyboardButtonMappings: [String: Int16] = [
|
||||
@objc public static let keyboardButtonMappings: [String: Int16] = [
|
||||
// Windows Key Codes
|
||||
"NULL": 0xFF,
|
||||
"BRUSH": 0xFF,
|
||||
"ERASER": 0xFF,
|
||||
"CTRL": 0x11, // VK_CONTROL
|
||||
"SHIFT": 0x10, // VK_SHIFT
|
||||
"ALT": 0x12, // VK_MENU
|
||||
@@ -383,7 +419,7 @@ import UIKit
|
||||
let defaults = UserDefaults.standard
|
||||
//if true { // save default entries if the data is empty.
|
||||
if defaults.data(forKey: "savedCommands") == nil { // save default entries if the data is empty.
|
||||
var defaultCommands: [RemoteCommand] = [
|
||||
let defaultCommands: [RemoteCommand] = [
|
||||
RemoteCommand(cmdString: "WIN", alias: "WIN"),
|
||||
RemoteCommand(cmdString: "F11", alias: "F11"),
|
||||
RemoteCommand(cmdString: "ESC", alias: "ESC"),
|
||||
@@ -410,22 +446,25 @@ import UIKit
|
||||
}
|
||||
|
||||
// extractKeyStrings from keyboardCMDString
|
||||
@objc public func extractKeyStringsFromComboCommand(from input: String) -> [String]? {
|
||||
let keys = CommandManager.keyboardButtonMappings.keys.joined(separator: "|")
|
||||
@objc public func extractAutoReleaseButtonStrings(from cmd: String) -> [String]? {
|
||||
let cmd = cmd.uppercased()
|
||||
let mergedKeys = (Set(CommandManager.keyboardButtonMappings.keys)
|
||||
.union(Set(CommandManager.mouseButtonMappings.keys)))
|
||||
let keys = mergedKeys.joined(separator: "|")
|
||||
let pattern = "^(?:(\(keys))(?:\\+(\(keys))*)*)$"
|
||||
|
||||
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
||||
print("Failed to create regex")
|
||||
return nil
|
||||
}
|
||||
let range = NSRange(location: 0, length: input.utf16.count)
|
||||
guard let match = regex.firstMatch(in: input, options: [], range: range) else {
|
||||
print("No match found for input: \(input)")
|
||||
let range = NSRange(location: 0, length: cmd.utf16.count)
|
||||
guard let match = regex.firstMatch(in: cmd, options: [], range: range) else {
|
||||
print("No match found for input: \(cmd)")
|
||||
return nil
|
||||
}
|
||||
// print("Regex matched for input: \(input)")
|
||||
|
||||
let matchedString = (input as NSString).substring(with: match.range(at: 0))
|
||||
let matchedString = (cmd as NSString).substring(with: match.range(at: 0))
|
||||
let keyStrings = matchedString.split(separator: "+").map { String($0) }
|
||||
|
||||
guard !keyStrings.isEmpty else {
|
||||
@@ -436,7 +475,8 @@ import UIKit
|
||||
var validKeyStrings: [String] = []
|
||||
|
||||
for key in keyStrings {
|
||||
if CommandManager.keyboardButtonMappings.keys.contains(key) {
|
||||
if (CommandManager.keyboardButtonMappings.keys.contains(key)
|
||||
|| CommandManager.mouseButtonMappings.keys.contains(key)) {
|
||||
validKeyStrings.append(key)
|
||||
} else {
|
||||
print(" '\(key)' is not defined in key mappings")
|
||||
@@ -458,10 +498,13 @@ import UIKit
|
||||
}
|
||||
|
||||
//super combo key button strings
|
||||
@objc public func extractSinglCmdStringsFromComboKeys(from input: String) -> [String]? {
|
||||
@objc public func extractCmdStrings(from input: String) -> [String]? {
|
||||
let input = input.uppercased()
|
||||
let combinedStrings = [CommandManager.keyboardButtonMappings.keys.map { $0 as String },
|
||||
CommandManager.oscButtonMappings.keys.map { $0 as String },
|
||||
CommandManager.mouseButtonMappings.keys.map { $0 as String },
|
||||
CommandManager.functionalButtonCmds.map { $0 as String },
|
||||
CommandManager.motionControlButtonCmds.map { $0 as String },
|
||||
CommandManager.touchPadCmds.map { $0 as String }
|
||||
]
|
||||
.lazy
|
||||
@@ -512,7 +555,7 @@ import UIKit
|
||||
@objc public func addCommand(_ command: RemoteCommand) -> Bool {
|
||||
command.cmdString = command.cmdString.uppercased() // convert all letters to upper case
|
||||
if(command.alias.trimmingCharacters(in: .whitespacesAndNewlines).count == 0) {command.alias = command.cmdString} // copy cmd string as alias when alias is empty
|
||||
let keyStrings = extractKeyStringsFromComboCommand(from: command.cmdString)
|
||||
let keyStrings = extractAutoReleaseButtonStrings(from: command.cmdString)
|
||||
if (keyStrings == nil) {return false} // in case of non-keyboard command strings, return false
|
||||
commands.append(command)
|
||||
saveCommands()
|
||||
@@ -551,38 +594,55 @@ import UIKit
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func saveCommands() {
|
||||
if let data = try? NSKeyedArchiver.archivedData(withRootObject: commands, requiringSecureCoding: false) {
|
||||
UserDefaults.standard.set(data, forKey: "savedCommands")
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func sendKeyComboCommand(keyboardCmdStrings: [String], delay: TimeInterval = 0.2, index: Int = 0) { // we need a large delay for WAN streaming
|
||||
@objc public func sendAutoReleaseComboCommand(cmdStrings: [String]?, delay: TimeInterval = 0.2, index: Int = 0, pressOnly: Bool = false, releaseOnly:Bool = false) { // we need a large delay for WAN streaming
|
||||
// 如果已处理完所有按键,则开始释放按键
|
||||
guard index < keyboardCmdStrings.count else {
|
||||
// 释放按键
|
||||
for keyStr in keyboardCmdStrings.reversed() { // 从后往前释放按键
|
||||
guard let cmdStrings = cmdStrings else { return }
|
||||
|
||||
if releaseOnly {
|
||||
for keyStr in cmdStrings.reversed() {
|
||||
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
|
||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_UP), 0) // 释放按键
|
||||
}
|
||||
if let mouseButtonCode = CommandManager.mouseButtonMappings[keyStr]{
|
||||
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), mouseButtonCode)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard index < cmdStrings.count else {
|
||||
// 释放按键
|
||||
DispatchQueue.main.asyncAfter(deadline: .now()) {
|
||||
if pressOnly {return}
|
||||
for keyStr in cmdStrings.reversed() { // 从后往前释放按键
|
||||
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
|
||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_UP), 0) // 释放按键
|
||||
}
|
||||
if let mouseButtonCode = CommandManager.mouseButtonMappings[keyStr]{
|
||||
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), mouseButtonCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
// 获取当前按键的映射值
|
||||
if let keyCode = CommandManager.keyboardButtonMappings[keyboardCmdStrings[index]] {
|
||||
if let keyCode = CommandManager.keyboardButtonMappings[cmdStrings[index]] {
|
||||
// 发送当前按键的按下事件
|
||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_DOWN), 0)
|
||||
|
||||
// 延迟后递归处理下一个按键
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||
self.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index + 1)
|
||||
}
|
||||
} else if let mouseButtonCode = CommandManager.mouseButtonMappings[cmdStrings[index]]{
|
||||
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), mouseButtonCode)
|
||||
} else {
|
||||
print("No mapping found for \(keyboardCmdStrings[index])")
|
||||
// 如果当前按键没有映射,跳过当前按键并继续下一个
|
||||
self.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index + 1)
|
||||
print("No mapping found for \(cmdStrings[index])")
|
||||
}
|
||||
// 延迟后递归处理下一个按键
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||
self.sendAutoReleaseComboCommand(cmdStrings: cmdStrings, delay: delay, index: index + 1, pressOnly: pressOnly, releaseOnly: releaseOnly)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,6 +662,4 @@ import UIKit
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
#import "ControllerSupport.h"
|
||||
#import "VoidController.h"
|
||||
|
||||
#import "VoidLink-Swift.h"
|
||||
#import "OnScreenControls.h"
|
||||
|
||||
#import "DataManager.h"
|
||||
@@ -42,17 +42,41 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
|
||||
NSLock *_controllerStreamLock;
|
||||
NSMutableDictionary *_voidControllers;
|
||||
id<ControllerSupportDelegate> _delegate;
|
||||
StreamConfiguration* _streamConfig;
|
||||
__weak id<ControllerSupportDelegate> _delegate;
|
||||
|
||||
float accumulatedDeltaX;
|
||||
float accumulatedDeltaY;
|
||||
float accumulatedScrollX;
|
||||
float accumulatedScrollY;
|
||||
|
||||
int _controllerMouseSwitch;
|
||||
bool _mouseSwitchButtonPressed;
|
||||
bool _mouseSwitchButtonBeingClicked;
|
||||
NSTimeInterval mouseSwitchDownTimestamp;
|
||||
int _controllerMouseLeftButton;
|
||||
int _controllerMouseRightButton;
|
||||
ControllerMouseStick _controllerMouseStick;
|
||||
bool _mapControllerToMouse;
|
||||
bool _controllerMouseEnabledFlag;
|
||||
float stickToMouseInputX;
|
||||
float stickToMouseInputY;
|
||||
float stickToWheelInputX;
|
||||
float stickToWheelInputY;
|
||||
float _stickToMouseExpo;
|
||||
float _stickToMouseVelocity;
|
||||
CADisplayLink *_displayLink;
|
||||
|
||||
float stickMaxOffset;
|
||||
float _leftStickMinOffset;
|
||||
float _rightStickMinOffset;
|
||||
|
||||
OnScreenControls *_osc;
|
||||
VoidController *_oscController;
|
||||
NSMutableSet* _activeGCControllers;
|
||||
TemporarySettings* tempSettings;
|
||||
OSCProfile* oscProfile;
|
||||
OSCProfilesManager* oscProfileMan;
|
||||
|
||||
#define EMULATING_SELECT 0x1
|
||||
#define EMULATING_SPECIAL 0x2
|
||||
@@ -64,6 +88,17 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
int _gyroMode;
|
||||
CGFloat _gyroSensitivity;
|
||||
bool _captureMouse;
|
||||
|
||||
bool _controllerGyroSwitchEnabled;
|
||||
bool _gyroEnabledFlag;
|
||||
int _controllerGyroSwitchToggle;
|
||||
int _controllerGyroSwitchHold;
|
||||
bool _reverseHoldButton;
|
||||
bool _controllerGyroSwitchTogglePressed;
|
||||
bool _controllerGyroSwitchHoldPressed;
|
||||
ControllerGyroSwitchMode _gyroSwitchMode;
|
||||
|
||||
__weak MotionHandler* motionHandler;
|
||||
}
|
||||
|
||||
// UPDATE_BUTTON_FLAG(controller, flag, pressed)
|
||||
@@ -75,16 +110,40 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor
|
||||
{
|
||||
VoidController* voidController = [_voidControllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
|
||||
|
||||
HapticEnginePreference preference = tempSettings.hapticEngine.intValue;
|
||||
|
||||
if (voidController == nil && controllerNumber == 0 && _oscEnabled) {
|
||||
// TODO: Rumble emulation for OSC
|
||||
// no physical controller connected:
|
||||
if(preference != RumbleOff){
|
||||
[_oscController.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
||||
[_oscController.highFreqMotor setMotorAmplitude:highFreqMotor];
|
||||
}
|
||||
}
|
||||
|
||||
if (voidController == nil) {
|
||||
// No connected controller for this player
|
||||
return;
|
||||
}
|
||||
|
||||
[voidController.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
||||
[voidController.highFreqMotor setMotorAmplitude:highFreqMotor];
|
||||
// physical controller connected:
|
||||
switch (preference) {
|
||||
case HapticEngineAuto:
|
||||
// if controller has no haptic profile, it already falled bakc to device engine
|
||||
[voidController.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
||||
[voidController.highFreqMotor setMotorAmplitude:highFreqMotor];
|
||||
break;
|
||||
case RumbleDevice:
|
||||
[_oscController.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
||||
[_oscController.highFreqMotor setMotorAmplitude:highFreqMotor];
|
||||
case LeftRightSwapped:
|
||||
[voidController.lowFreqMotor setMotorAmplitude:highFreqMotor];
|
||||
[voidController.highFreqMotor setMotorAmplitude:lowFreqMotor];
|
||||
case RumbleOff:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
-(void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger
|
||||
@@ -101,15 +160,20 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
[controller.rightTriggerMotor setMotorAmplitude:rightTrigger];
|
||||
}
|
||||
|
||||
- (bool)isLandscapeNow {
|
||||
return CGRectGetWidth([[UIScreen mainScreen]bounds]) > CGRectGetHeight([[UIScreen mainScreen]bounds]);
|
||||
}
|
||||
|
||||
-(void)updateTimerStateForController:(VoidController* )voidController{
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
// if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
if (true) {
|
||||
if(_gyroMode == GyroModeOff){
|
||||
[self stopTimerForController:voidController];
|
||||
return;
|
||||
}
|
||||
|
||||
if(voidController.gamepad.motion.hasAttitudeAndRotationRate) [voidController.motionTypes addObject:@(LI_MOTION_TYPE_ACCEL)];
|
||||
if(voidController.gamepad.motion.hasRotationRate) [voidController.motionTypes addObject:@(LI_MOTION_TYPE_GYRO)];
|
||||
if (@available(iOS 14.0, *)) if(voidController.gamepad.motion.hasRotationRate) [voidController.motionTypes addObject:@(LI_MOTION_TYPE_GYRO)];
|
||||
|
||||
for(NSNumber* motionTypeObj in voidController.motionTypes){
|
||||
uint8_t motionType = motionTypeObj.intValue;
|
||||
@@ -147,20 +211,29 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
voidController.lastDeviceAccelSample = deviceAccelSample;
|
||||
|
||||
// Convert g to m/s^2
|
||||
if(UIApplication.sharedApplication.windows.firstObject.windowScene.interfaceOrientation == 4){ //check for landscape left or landscape right
|
||||
if (@available(iOS 13.0, *)) {
|
||||
if(UIApplication.sharedApplication.windows.firstObject.windowScene.interfaceOrientation == 4){ //check for landscape left or landscape right
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_ACCEL,
|
||||
deviceAccelSample.y * -9.80665f * self->_gyroSensitivity,
|
||||
deviceAccelSample.z * -9.80665f * self->_gyroSensitivity,
|
||||
deviceAccelSample.x * -9.80665f * self->_gyroSensitivity);
|
||||
}
|
||||
else{
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_ACCEL,
|
||||
deviceAccelSample.y * +9.80665f * self->_gyroSensitivity,
|
||||
deviceAccelSample.z * -9.80665f * self->_gyroSensitivity,
|
||||
deviceAccelSample.x * +9.80665f * self->_gyroSensitivity);
|
||||
}
|
||||
}
|
||||
else{
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_ACCEL,
|
||||
deviceAccelSample.y * -9.80665f * self->_gyroSensitivity,
|
||||
deviceAccelSample.z * -9.80665f * self->_gyroSensitivity,
|
||||
deviceAccelSample.x * -9.80665f * self->_gyroSensitivity);
|
||||
}
|
||||
else{
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_ACCEL,
|
||||
deviceAccelSample.y * +9.80665f * self->_gyroSensitivity,
|
||||
deviceAccelSample.z * -9.80665f * self->_gyroSensitivity,
|
||||
deviceAccelSample.x * +9.80665f * self->_gyroSensitivity);
|
||||
}
|
||||
}];
|
||||
});}
|
||||
break;
|
||||
@@ -186,21 +259,68 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
voidController.lastDeviceGyroSample = deviceGyroSample;
|
||||
|
||||
// Convert rad/s to deg/s
|
||||
//NSLog(@"sending built-in gyro data, accelSample data 00: %f, playerIndex: %d",deviceGyroSample.x, voidController.playerIndex);
|
||||
|
||||
UIInterfaceOrientation interfaceOrientation = UIInterfaceOrientationUnknown;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
interfaceOrientation = UIApplication.sharedApplication.windows.firstObject.windowScene.interfaceOrientation;
|
||||
} else {
|
||||
interfaceOrientation = UIApplication.sharedApplication.statusBarOrientation;
|
||||
}
|
||||
|
||||
switch (interfaceOrientation) {
|
||||
case UIInterfaceOrientationLandscapeLeft:
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_GYRO,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0);
|
||||
break;
|
||||
case UIInterfaceOrientationLandscapeRight:
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_GYRO,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.y * -57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.x * -57.2957795f * self->_gyroSensitivity : 0);
|
||||
break;
|
||||
case UIInterfaceOrientationPortrait:
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_GYRO,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.y * -57.2957795f * self->_gyroSensitivity : 0);
|
||||
break;
|
||||
case UIInterfaceOrientationPortraitUpsideDown:
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_GYRO,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.x * -57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0);
|
||||
break;
|
||||
default:
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_GYRO,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0);
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
if(UIApplication.sharedApplication.windows.firstObject.windowScene.interfaceOrientation == 4){//check for landscape left or landscape right
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_GYRO,
|
||||
deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity,
|
||||
deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity,
|
||||
deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity);
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0);
|
||||
}
|
||||
else{
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_GYRO,
|
||||
deviceGyroSample.y * -57.2957795f * self->_gyroSensitivity,
|
||||
deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity,
|
||||
deviceGyroSample.x * -57.2957795f * self->_gyroSensitivity);
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.y * -57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? deviceGyroSample.x * -57.2957795f * self->_gyroSensitivity : 0);
|
||||
}
|
||||
*/
|
||||
}];
|
||||
break;
|
||||
}
|
||||
@@ -209,72 +329,75 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
#endif
|
||||
else{
|
||||
NSLog(@"controller obj timer update: controller timer ");
|
||||
switch (motionType) {
|
||||
case LI_MOTION_TYPE_ACCEL:
|
||||
[voidController.accelTimer invalidate];
|
||||
voidController.accelTimer = nil;
|
||||
|
||||
if (voidController.reportRateHz && voidController.gamepad.motion.hasGravityAndUserAcceleration) {
|
||||
// Reset the last motion sample
|
||||
GCAcceleration emptyAccelSample = {};
|
||||
voidController.lastAccelSample = emptyAccelSample;
|
||||
NSLog(@"setup controller gyro accelTimer");
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
voidController.hasAccelerometer = YES;
|
||||
voidController.accelTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) {
|
||||
// Don't send duplicate samples
|
||||
GCAcceleration lastAccelSample = voidController.lastAccelSample;
|
||||
GCAcceleration accelSample = voidController.gamepad.motion.acceleration;
|
||||
if (@available(iOS 14.0, *)) {
|
||||
switch (motionType) {
|
||||
case LI_MOTION_TYPE_ACCEL:
|
||||
[voidController.accelTimer invalidate];
|
||||
voidController.accelTimer = nil;
|
||||
|
||||
if (memcmp(&accelSample, &lastAccelSample, sizeof(accelSample)) == 0) {
|
||||
return;
|
||||
}
|
||||
voidController.lastAccelSample = accelSample;
|
||||
if (voidController.reportRateHz && voidController.gamepad.motion.hasGravityAndUserAcceleration) {
|
||||
// Reset the last motion sample
|
||||
GCAcceleration emptyAccelSample = {};
|
||||
voidController.lastAccelSample = emptyAccelSample;
|
||||
NSLog(@"setup controller gyro accelTimer");
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
voidController.hasAccelerometer = YES;
|
||||
voidController.accelTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) {
|
||||
// Don't send duplicate samples
|
||||
GCAcceleration lastAccelSample = voidController.lastAccelSample;
|
||||
GCAcceleration accelSample = voidController.gamepad.motion.acceleration;
|
||||
|
||||
// Convert g to m/s^2
|
||||
//NSLog(@"sending controller gyro data, accelSample data 00: %f, playerIndex: %ld, obj: %@",accelSample.x, (long)voidController.gamepad.playerIndex, voidController);
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_ACCEL,
|
||||
accelSample.x * -9.80665f * self->_gyroSensitivity,
|
||||
accelSample.y * -9.80665f * self->_gyroSensitivity,
|
||||
accelSample.z * -9.80665f * self->_gyroSensitivity);
|
||||
}];
|
||||
});
|
||||
}
|
||||
break;
|
||||
if (memcmp(&accelSample, &lastAccelSample, sizeof(accelSample)) == 0) {
|
||||
return;
|
||||
}
|
||||
voidController.lastAccelSample = accelSample;
|
||||
|
||||
case LI_MOTION_TYPE_GYRO:
|
||||
[voidController.gyroTimer invalidate];
|
||||
voidController.gyroTimer = nil;
|
||||
// Convert g to m/s^2
|
||||
//NSLog(@"sending controller gyro data, accelSample data 00: %f, playerIndex: %ld, obj: %@",accelSample.x, (long)voidController.gamepad.playerIndex, voidController);
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_ACCEL,
|
||||
accelSample.x * -9.80665f * self->_gyroSensitivity,
|
||||
accelSample.y * -9.80665f * self->_gyroSensitivity,
|
||||
accelSample.z * -9.80665f * self->_gyroSensitivity);
|
||||
}];
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
if (voidController.reportRateHz && voidController.gamepad.motion.hasRotationRate) {
|
||||
// Reset the last motion sample
|
||||
GCRotationRate emptyGyroSample = {};
|
||||
voidController.lastGyroSample = emptyGyroSample;
|
||||
//dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
{dispatch_async(dispatch_get_main_queue(), ^{
|
||||
voidController.hasGyroscope = YES;
|
||||
voidController.gyroTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) {
|
||||
// Don't send duplicate samples
|
||||
GCRotationRate lastGyroSample = voidController.lastGyroSample;
|
||||
GCRotationRate gyroSample = voidController.gamepad.motion.rotationRate;
|
||||
if (memcmp(&gyroSample, &lastGyroSample, sizeof(gyroSample)) == 0) {
|
||||
return;
|
||||
}
|
||||
voidController.lastGyroSample = gyroSample;
|
||||
case LI_MOTION_TYPE_GYRO:
|
||||
[voidController.gyroTimer invalidate];
|
||||
voidController.gyroTimer = nil;
|
||||
|
||||
// Convert rad/s to deg/s
|
||||
// NSLog(@"sending controller gyro data, gyroSample data 00: %f, playerIndex: %ld, obj: %@",gyroSample.x, (long)voidController.gamepad.playerIndex, voidController);
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_GYRO,
|
||||
gyroSample.x * 57.2957795f * self->_gyroSensitivity,
|
||||
gyroSample.z * 57.2957795f * self->_gyroSensitivity,
|
||||
gyroSample.y * -57.2957795f * self->_gyroSensitivity);
|
||||
}];
|
||||
// });
|
||||
});}
|
||||
}
|
||||
break;
|
||||
if (voidController.reportRateHz && voidController.gamepad.motion.hasRotationRate) {
|
||||
// Reset the last motion sample
|
||||
GCRotationRate emptyGyroSample = {};
|
||||
voidController.lastGyroSample = emptyGyroSample;
|
||||
//dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
{dispatch_async(dispatch_get_main_queue(), ^{
|
||||
voidController.hasGyroscope = YES;
|
||||
voidController.gyroTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) {
|
||||
// Don't send duplicate samples
|
||||
GCRotationRate lastGyroSample = voidController.lastGyroSample;
|
||||
GCRotationRate gyroSample = voidController.gamepad.motion.rotationRate;
|
||||
if (memcmp(&gyroSample, &lastGyroSample, sizeof(gyroSample)) == 0) {
|
||||
return;
|
||||
}
|
||||
voidController.lastGyroSample = gyroSample;
|
||||
|
||||
// Convert rad/s to deg/s
|
||||
// NSLog(@"sending controller gyro data, gyroSample data 00: %f, playerIndex: %ld, obj: %@",gyroSample.x, (long)voidController.gamepad.playerIndex, voidController);
|
||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||
LI_MOTION_TYPE_GYRO,
|
||||
self->_gyroEnabledFlag ? gyroSample.x * 57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? gyroSample.z * 57.2957795f * self->_gyroSensitivity : 0,
|
||||
self->_gyroEnabledFlag ? gyroSample.y * -57.2957795f * self->_gyroSensitivity : 0);
|
||||
}];
|
||||
// });
|
||||
});}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,7 +406,7 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
|
||||
// Set the motion sensor state if they require manual activation
|
||||
[self updateSensorSateForController:voidController];
|
||||
NSLog(@"sensor active: %d", voidController.gamepad.motion.sensorsActive);
|
||||
// NSLog(@"sensor active: %d", voidController.gamepad.motion.sensorsActive);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +424,7 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
}
|
||||
|
||||
- (void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz {
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
//if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
NSLog(@"gyroMode: %ld", (long)_gyroMode);
|
||||
|
||||
VoidController* voidController = [_voidControllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
|
||||
@@ -322,7 +445,7 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
voidController.controllerNumber = controllerNumber;
|
||||
|
||||
if(voidController == _oscController) [self updateTimerStateForController:voidController];
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
-(void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b {
|
||||
@@ -879,6 +1002,63 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
}
|
||||
}
|
||||
|
||||
double rc_expo(double x, double expo) {
|
||||
if (x == 0.0) return 0.0;
|
||||
|
||||
double ax = fabs(x);
|
||||
double y = pow(ax, expo);
|
||||
|
||||
return x > 0 ? y : -y;
|
||||
}
|
||||
|
||||
- (void)sendStickToMouseMoveEventWithStickX:(float)stickX stickY:(float)stickY expo:(float)expo {
|
||||
CGFloat mouseDeltaX = _stickToMouseVelocity*rc_expo(stickX, expo);
|
||||
CGFloat mouseDeltaY = _stickToMouseVelocity*rc_expo(stickY, expo);
|
||||
LiSendMouseMoveEvent(mouseDeltaX, -mouseDeltaY);
|
||||
}
|
||||
|
||||
- (void)stopDisplayLink {
|
||||
[_displayLink invalidate];
|
||||
_displayLink = nil;
|
||||
}
|
||||
|
||||
- (void)displayLinkCallBack {
|
||||
if(!_controllerMouseEnabledFlag){
|
||||
return;
|
||||
}
|
||||
NSTimeInterval delay = 0.5/_displayLink.preferredFramesPerSecond*NSEC_PER_SEC;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[self sendStickToMouseMoveEventWithStickX:self->stickToMouseInputX stickY:self->stickToMouseInputY expo:self->_stickToMouseExpo];
|
||||
LiSendHighResScrollEvent(15*self->stickToWheelInputY);
|
||||
LiSendHighResHScrollEvent(15*self->stickToWheelInputX);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)sendControllerMouseSwitchClick:(VoidController*) voidController{
|
||||
[self setButtonFlag:voidController flags:self->_controllerMouseSwitch];
|
||||
_mouseSwitchButtonBeingClicked = true;
|
||||
[self updateFinished:voidController];
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.03*NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[self clearButtonFlag:voidController flags:self->_controllerMouseSwitch];
|
||||
self->_mouseSwitchButtonBeingClicked = false;
|
||||
[self updateFinished:voidController];
|
||||
});
|
||||
}
|
||||
|
||||
- (bool)useMotionHandler{
|
||||
return self->tempSettings.emulatedControllerType.intValue!=LI_CTYPE_PS
|
||||
||self->tempSettings.gyroMode.intValue==GyroModeOff;
|
||||
}
|
||||
|
||||
- (void)switchGyroOnOffByControllerButton{
|
||||
self->_gyroEnabledFlag = self->_gyroEnabledFlag && self->_controllerGyroSwitchEnabled;
|
||||
if([self useMotionHandler]){
|
||||
if(self->_gyroEnabledFlag) [self->motionHandler startGyroByControllerButton];
|
||||
else [self->motionHandler stopGyroUpdateWithInterruptNoneGyroInput:false resetLeftStick:true];
|
||||
}
|
||||
else self->_gyroEnabledFlag = self->_gyroEnabledFlag || !self->_controllerGyroSwitchEnabled;
|
||||
}
|
||||
|
||||
-(void) registerControllerCallbacks:(GCController*) controller
|
||||
{
|
||||
if (controller != NULL) {
|
||||
@@ -923,12 +1103,190 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
}
|
||||
}
|
||||
|
||||
controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) {
|
||||
if(self->_swapABXYButtons){
|
||||
switch (self->_controllerMouseLeftButton) {
|
||||
case ControllerButtonA:
|
||||
self->_controllerMouseLeftButton = ControllerButtonB;
|
||||
break;
|
||||
case ControllerButtonB:
|
||||
self->_controllerMouseLeftButton = ControllerButtonA;
|
||||
break;
|
||||
case ControllerButtonX:
|
||||
self->_controllerMouseLeftButton = ControllerButtonY;
|
||||
break;
|
||||
case ControllerButtonY:
|
||||
self->_controllerMouseLeftButton = ControllerButtonX;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (self->_controllerMouseRightButton) {
|
||||
case ControllerButtonA:
|
||||
self->_controllerMouseRightButton = ControllerButtonB;
|
||||
break;
|
||||
case ControllerButtonB:
|
||||
self->_controllerMouseRightButton = ControllerButtonA;
|
||||
break;
|
||||
case ControllerButtonX:
|
||||
self->_controllerMouseRightButton = ControllerButtonY;
|
||||
break;
|
||||
case ControllerButtonY:
|
||||
self->_controllerMouseRightButton = ControllerButtonX;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[ControllerUtil listenWithController:controller swapABXY:self->_swapABXYButtons handler:^(NSDictionary * buttonDict, GCExtendedGamepad * gamepad, GCControllerElement * element) {
|
||||
VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
|
||||
short leftStickX, leftStickY;
|
||||
short rightStickX, rightStickY;
|
||||
unsigned char leftTrigger, rightTrigger;
|
||||
|
||||
for(NSNumber* buttonFlagId in buttonDict){
|
||||
GCControllerButtonInput * button = (GCControllerButtonInput *)buttonDict[buttonFlagId];
|
||||
if(self->_mapControllerToMouse){
|
||||
if(button.pressed){
|
||||
if(buttonFlagId.intValue == self->_controllerMouseSwitch){
|
||||
self->_mouseSwitchButtonPressed = true;
|
||||
self->mouseSwitchDownTimestamp = CACurrentMediaTime();
|
||||
}
|
||||
}
|
||||
else{
|
||||
if(buttonFlagId.intValue == self->_controllerMouseSwitch && self->_mouseSwitchButtonPressed){
|
||||
if(CACurrentMediaTime()-self->mouseSwitchDownTimestamp>1){
|
||||
self->_controllerMouseEnabledFlag = !self->_controllerMouseEnabledFlag;
|
||||
[self updateLeftStick:voidController x:0 y:0];
|
||||
[self updateRightStick:voidController x:0 y:0];
|
||||
}
|
||||
else [self sendControllerMouseSwitchClick:voidController];
|
||||
self->_mouseSwitchButtonPressed = false;
|
||||
self->mouseSwitchDownTimestamp = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else self->_controllerMouseEnabledFlag = false;
|
||||
|
||||
// controller switch buttons
|
||||
if(true){
|
||||
if(button.pressed){
|
||||
if (buttonFlagId.intValue == self->_controllerGyroSwitchToggle
|
||||
&& !self->_controllerGyroSwitchTogglePressed) {
|
||||
self->_controllerGyroSwitchTogglePressed = true;
|
||||
|
||||
self->_gyroEnabledFlag = !self->_gyroEnabledFlag;
|
||||
[self switchGyroOnOffByControllerButton];
|
||||
}
|
||||
if (buttonFlagId.intValue == self->_controllerGyroSwitchHold
|
||||
&& !self->_controllerGyroSwitchHoldPressed) {
|
||||
self->_controllerGyroSwitchHoldPressed = true;
|
||||
|
||||
self->_gyroEnabledFlag = !self->_reverseHoldButton;
|
||||
[self switchGyroOnOffByControllerButton];
|
||||
}
|
||||
}
|
||||
else{
|
||||
if (buttonFlagId.intValue == self->_controllerGyroSwitchToggle
|
||||
&& self->_controllerGyroSwitchTogglePressed) {
|
||||
self->_controllerGyroSwitchTogglePressed = false;
|
||||
}
|
||||
if (buttonFlagId.intValue == self->_controllerGyroSwitchHold
|
||||
&& self->_controllerGyroSwitchHoldPressed) {
|
||||
self->_controllerGyroSwitchHoldPressed = false;
|
||||
|
||||
self->_gyroEnabledFlag = self->_reverseHoldButton;
|
||||
[self switchGyroOnOffByControllerButton];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(self->_controllerMouseEnabledFlag){
|
||||
if(buttonFlagId.intValue == self->_controllerMouseLeftButton || buttonFlagId.intValue == self->_controllerMouseRightButton){
|
||||
if(buttonFlagId.intValue == self->_controllerMouseLeftButton) LiSendMouseButtonEvent(button.pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
if(buttonFlagId.intValue == self->_controllerMouseRightButton) LiSendMouseButtonEvent(button.pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
UPDATE_BUTTON_FLAG(voidController, buttonFlagId.intValue, NO);
|
||||
}
|
||||
else if(buttonFlagId.intValue!=self->_controllerMouseSwitch || !self->_mapControllerToMouse) UPDATE_BUTTON_FLAG(voidController, buttonFlagId.intValue, button.pressed);
|
||||
}
|
||||
else if(buttonFlagId.intValue!=self->_controllerMouseSwitch || !self->_mapControllerToMouse) UPDATE_BUTTON_FLAG(voidController, buttonFlagId.intValue, button.pressed);
|
||||
}
|
||||
|
||||
CGFloat leftStickXRaw = gamepad.leftThumbstick.xAxis.value * self->stickMaxOffset;
|
||||
CGFloat leftStickYRaw = gamepad.leftThumbstick.yAxis.value * self->stickMaxOffset;
|
||||
|
||||
CGFloat rightStickXRaw = gamepad.rightThumbstick.xAxis.value * self->stickMaxOffset;
|
||||
CGFloat rightStickYRaw = gamepad.rightThumbstick.yAxis.value * self->stickMaxOffset;
|
||||
|
||||
|
||||
CGVector leftStickOffset = [ControllerUtil compensatedWithOffsetVector:CGVectorMake(leftStickXRaw, leftStickYRaw) minOffset:self->_leftStickMinOffset circulate:false];
|
||||
|
||||
CGVector rightStickOffset = [ControllerUtil compensatedWithOffsetVector:CGVectorMake(rightStickXRaw, rightStickYRaw) minOffset:self->_rightStickMinOffset circulate:false];
|
||||
|
||||
leftStickX = self->_controllerMouseEnabledFlag ? 0 : leftStickOffset.dx;
|
||||
leftStickY = self->_controllerMouseEnabledFlag ? 0 : leftStickOffset.dy;
|
||||
|
||||
rightStickX = self->_controllerMouseEnabledFlag ? 0 : rightStickOffset.dx;
|
||||
rightStickY = self->_controllerMouseEnabledFlag ? 0 : rightStickOffset.dy;
|
||||
|
||||
if(self->_controllerMouseEnabledFlag){
|
||||
self->stickToMouseInputX = self->_controllerMouseStick == LeftStickToMouse ? gamepad.leftThumbstick.xAxis.value : gamepad.rightThumbstick.xAxis.value;
|
||||
self->stickToMouseInputY = self->_controllerMouseStick == LeftStickToMouse ? gamepad.leftThumbstick.yAxis.value : gamepad.rightThumbstick.yAxis.value;
|
||||
|
||||
self->stickToWheelInputX = self->_controllerMouseStick == LeftStickToMouse ? gamepad.rightThumbstick.xAxis.value: gamepad.leftThumbstick.xAxis.value;
|
||||
self->stickToWheelInputY = self->_controllerMouseStick == LeftStickToMouse ? gamepad.rightThumbstick.yAxis.value: gamepad.leftThumbstick.yAxis.value;
|
||||
}
|
||||
else{
|
||||
self->stickToMouseInputX = 0;
|
||||
self->stickToMouseInputY = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
if(self->oscProfile.mapGyroTo!=mapGyroToControllerStick
|
||||
||!self->oscProfile.rollToLeftStick) [self updateLeftStick:voidController x:leftStickX y:leftStickY];
|
||||
*/
|
||||
|
||||
if([self useMotionHandler]
|
||||
&& self->oscProfile.mapGyroTo==mapGyroToControllerStick
|
||||
&& self->oscProfile.yawPitchToRightStick
|
||||
&& self->_gyroEnabledFlag
|
||||
) [self->motionHandler mixPhysicalRightStickAndGyroInputWithX:rightStickX y:rightStickY];
|
||||
else [self updateRightStick: voidController.playerIndex==0?self->_oscController:voidController x:rightStickX y:rightStickY];
|
||||
|
||||
if([self useMotionHandler]
|
||||
&& self->oscProfile.mapGyroTo==mapGyroToControllerStick
|
||||
&& self->oscProfile.rollToLeftStick
|
||||
&& self->_gyroEnabledFlag
|
||||
) [self->motionHandler mixPhysicalLeftStickAndGyroInputWithX:leftStickX y:leftStickY];
|
||||
else [self updateLeftStick: voidController.playerIndex==0?self->_oscController:voidController x:leftStickX y:leftStickY];
|
||||
|
||||
leftTrigger = gamepad.leftTrigger.value * 0xFF;
|
||||
rightTrigger = gamepad.rightTrigger.value * 0xFF;
|
||||
[self updateTriggers:voidController left:leftTrigger right:rightTrigger];
|
||||
|
||||
[self updateFinished:voidController];
|
||||
|
||||
if (@available(iOS 14.0, *)) {
|
||||
if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]) {
|
||||
[self handleControllerTouchpad:voidController
|
||||
touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]
|
||||
index:0];
|
||||
}
|
||||
if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]) {
|
||||
[self handleControllerTouchpad:voidController
|
||||
touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]
|
||||
index:1];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
/*
|
||||
controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) {
|
||||
VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
|
||||
short leftStickX, leftStickY;
|
||||
short rightStickX, rightStickY;
|
||||
unsigned char leftTrigger, rightTrigger;
|
||||
if (self->_swapABXYButtons) {
|
||||
UPDATE_BUTTON_FLAG(voidController, B_FLAG, gamepad.buttonA.pressed);
|
||||
UPDATE_BUTTON_FLAG(voidController, A_FLAG, gamepad.buttonB.pressed);
|
||||
@@ -1012,11 +1370,14 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
}
|
||||
}
|
||||
|
||||
leftStickX = gamepad.leftThumbstick.xAxis.value * 0x7FFE;
|
||||
leftStickY = gamepad.leftThumbstick.yAxis.value * 0x7FFE;
|
||||
leftStickX = self->controllerToMouseStick != LeftStickToMouse ? gamepad.leftThumbstick.xAxis.value * 0x7FFE : 0;
|
||||
leftStickY = self->controllerToMouseStick != LeftStickToMouse ? gamepad.leftThumbstick.yAxis.value * 0x7FFE : 0;;
|
||||
|
||||
rightStickX = gamepad.rightThumbstick.xAxis.value * 0x7FFE;
|
||||
rightStickY = gamepad.rightThumbstick.yAxis.value * 0x7FFE;
|
||||
rightStickX = self->controllerToMouseStick != RightStickToMouse ? gamepad.rightThumbstick.xAxis.value * 0x7FFE : 0;
|
||||
rightStickY = self->controllerToMouseStick != RightStickToMouse ? gamepad.rightThumbstick.yAxis.value * 0x7FFE : 0;
|
||||
|
||||
self->stickToMouseInputX = self->controllerToMouseStick == LeftStickToMouse ? gamepad.leftThumbstick.xAxis.value : gamepad.rightThumbstick.xAxis.value;
|
||||
self->stickToMouseInputY = self->controllerToMouseStick == LeftStickToMouse ? gamepad.leftThumbstick.yAxis.value : gamepad.rightThumbstick.yAxis.value;
|
||||
|
||||
leftTrigger = gamepad.leftTrigger.value * 0xFF;
|
||||
rightTrigger = gamepad.rightTrigger.value * 0xFF;
|
||||
@@ -1026,6 +1387,7 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
[self updateTriggers:voidController left:leftTrigger right:rightTrigger];
|
||||
[self updateFinished:voidController];
|
||||
};
|
||||
*/
|
||||
}
|
||||
} else {
|
||||
Log(LOG_W, @"Tried to register controller callbacks on NULL controller");
|
||||
@@ -1360,10 +1722,55 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
|
||||
_oscController.playerIndex = 0;
|
||||
|
||||
oscProfile = [oscProfileMan getSelectedProfile];
|
||||
motionHandler = [MotionHandler sharedWithProfile:oscProfile];
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
TemporarySettings* currentSettings = [dataMan getSettings];
|
||||
_oscEnabled = _oscEnabled || (OnScreenControlsLevel)[currentSettings.onscreenControls integerValue] != OnScreenControlsLevelOff || streamConfig.gyroMode != GyroModeOff;
|
||||
_gyroSensitivity = currentSettings.gyroSensitivity.floatValue;
|
||||
tempSettings = [dataMan getSettings];
|
||||
|
||||
_oscEnabled = _oscEnabled || (OnScreenControlsLevel)[tempSettings.onscreenControls integerValue] != OnScreenControlsLevelOff || streamConfig.gyroMode != GyroModeOff;
|
||||
_gyroSensitivity = tempSettings.gyroSensitivity.floatValue;
|
||||
|
||||
_mapControllerToMouse = tempSettings.mapControllerToMouse;
|
||||
_controllerMouseSwitch = tempSettings.controllerMouseSwitch.intValue;
|
||||
mouseSwitchDownTimestamp = 0;
|
||||
_mouseSwitchButtonPressed = false;
|
||||
_mouseSwitchButtonBeingClicked = false;
|
||||
_controllerMouseStick = tempSettings.controllerMouseStick.intValue;
|
||||
_controllerMouseLeftButton = tempSettings.controllerMouseLeftButton.intValue;
|
||||
_controllerMouseRightButton = tempSettings.controllerMouseRightButton.intValue;
|
||||
_stickToMouseExpo = tempSettings.controllerMouseExpo.floatValue;
|
||||
_stickToMouseVelocity = tempSettings.controllerMousePointerVelocity.floatValue*60/tempSettings.framerate.intValue;
|
||||
stickToMouseInputX = 0;
|
||||
stickToMouseInputY = 0;
|
||||
stickToWheelInputY = 0;
|
||||
[self stopDisplayLink];
|
||||
if(_mapControllerToMouse){
|
||||
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallBack)];
|
||||
if (@available(iOS 15.0, tvOS 15.0, *)) {
|
||||
[_displayLink setPreferredFrameRateRange:CAFrameRateRangeMake(tempSettings.framerate.intValue,tempSettings.framerate.intValue, tempSettings.framerate.intValue)];
|
||||
}
|
||||
else {
|
||||
_displayLink.preferredFramesPerSecond = tempSettings.framerate.intValue;
|
||||
}
|
||||
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
||||
}
|
||||
|
||||
_controllerGyroSwitchEnabled = oscProfile.controllerGyroSwitchMode != ControllerGyroSwitchDisabled;
|
||||
_gyroSwitchMode = oscProfile.controllerGyroSwitchMode;
|
||||
// _gyroSwitchMode = ControllerGyroSwitchHoldDownToActivate;
|
||||
_controllerGyroSwitchToggle = oscProfile.controllerGyroSwitchToggle;
|
||||
_controllerGyroSwitchHold = oscProfile.controllerGyroSwitchHold;
|
||||
_reverseHoldButton = oscProfile.reverseGyroHoldButton;
|
||||
_controllerGyroSwitchTogglePressed = false;
|
||||
_controllerGyroSwitchHoldPressed = false;
|
||||
|
||||
stickMaxOffset = 0x7FFE;
|
||||
_leftStickMinOffset = oscProfile.physicalLeftStickMinOffset;
|
||||
_rightStickMinOffset = oscProfile.physicalRightStickMinOffset;
|
||||
|
||||
if(oscProfile.controllerGyroSwitchMode == ControllerGyroSwitchDisabled && ![self useMotionHandler]) _gyroEnabledFlag = true;
|
||||
|
||||
if(![self useMotionHandler]) [self->motionHandler stopGyroUpdateWithInterruptNoneGyroInput:false resetLeftStick:true];
|
||||
}
|
||||
|
||||
- (void)resetGyroInputForController:(VoidController* )voidController{
|
||||
@@ -1553,8 +1960,14 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
|
||||
|
||||
_oscController = [[VoidController alloc] init];
|
||||
[self initializeControllerHaptics:_oscController];
|
||||
_gyroMode = AlwaysDevice;
|
||||
|
||||
_controllerMouseEnabledFlag = false;
|
||||
|
||||
_gyroEnabledFlag = false;
|
||||
oscProfileMan = [OSCProfilesManager sharedManager:CGRectZero];
|
||||
|
||||
[self updateCommonConfig:streamConfig];
|
||||
|
||||
for(VoidController* voidController in _voidControllers.allValues){
|
||||
@@ -1568,6 +1981,7 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
_shallDisableGyroHotSwitch = streamConfig.gyroMode == GyroModeOff && _voidControllers.allValues.count == 0;
|
||||
NSLog(@"shallDisableGyroHotSwitch %d", _shallDisableGyroHotSwitch);
|
||||
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -1693,6 +2107,8 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
||||
[self unregisterMouseCallbacks:mouse];
|
||||
}
|
||||
}
|
||||
|
||||
[self stopDisplayLink];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
//
|
||||
// ControllerUtil.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025/11/19.
|
||||
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
import GameController
|
||||
|
||||
@objc enum ControllerButton: Int {
|
||||
// Face buttons
|
||||
case a = 0x1000
|
||||
case b = 0x2000
|
||||
case x = 0x4000
|
||||
case y = 0x8000
|
||||
|
||||
// DPad
|
||||
case dpadUp = 0x0001
|
||||
case dpadDown = 0x0002
|
||||
case dpadLeft = 0x0004
|
||||
case dpadRight = 0x0008
|
||||
|
||||
// Shoulder
|
||||
case leftShoulder = 0x0100
|
||||
case rightShoulder = 0x0200
|
||||
|
||||
// Stick click
|
||||
case leftStickButton = 0x0040
|
||||
case rightStickButton = 0x0080
|
||||
|
||||
// Menu/Back/Special
|
||||
case menu = 0x0010 // PLAY_FLAG
|
||||
case back = 0x0020 // BACK_FLAG
|
||||
case special = 0x0400
|
||||
|
||||
// Extended buttons (Sunshine only)
|
||||
case paddle1 = 0x010000
|
||||
case paddle2 = 0x020000
|
||||
case paddle3 = 0x040000
|
||||
case paddle4 = 0x080000
|
||||
case touchpadButton = 0x100000
|
||||
case misc = 0x200000
|
||||
|
||||
case leftTrigger = 0x400000
|
||||
case rightTrigger = 0x800000
|
||||
|
||||
case null = 0xFFFFFF
|
||||
}
|
||||
|
||||
|
||||
@objc class ControllerUtil: NSObject {
|
||||
|
||||
static private let stickMaxOffset:CGFloat = 0x7FFE
|
||||
|
||||
@objc static func listen(
|
||||
controller: GCController,
|
||||
swapABXY: Bool,
|
||||
handler: @escaping (_ buttonDict: NSDictionary,
|
||||
_ gamepad: GCExtendedGamepad,
|
||||
_ element: GCControllerElement) -> Void
|
||||
) {
|
||||
guard let gamepad = controller.extendedGamepad else { return }
|
||||
|
||||
// 内部生成 map
|
||||
var tempMap: [NSNumber: GCControllerButtonInput] = [:]
|
||||
let swiftMap = buildMapping(for: controller, swapABXY: swapABXY)
|
||||
for (button, input) in swiftMap {
|
||||
tempMap[NSNumber(value: button.rawValue)] = input
|
||||
}
|
||||
let buttonDict = tempMap as NSDictionary
|
||||
|
||||
// 单一 gamepad.valueChangedHandler
|
||||
gamepad.valueChangedHandler = { gamepad, element in
|
||||
handler(buttonDict, gamepad, element)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - 构建按钮映射
|
||||
private static func buildMapping(for controller: GCController, swapABXY:Bool)
|
||||
-> [ControllerButton : GCControllerButtonInput]
|
||||
{
|
||||
var result: [ControllerButton : GCControllerButtonInput] = [:]
|
||||
|
||||
if let pad = controller.extendedGamepad {
|
||||
// Face buttons
|
||||
if swapABXY {
|
||||
result[.a] = pad.buttonB
|
||||
result[.b] = pad.buttonA
|
||||
result[.x] = pad.buttonY
|
||||
result[.y] = pad.buttonX
|
||||
}
|
||||
else{
|
||||
result[.a] = pad.buttonA
|
||||
result[.b] = pad.buttonB
|
||||
result[.x] = pad.buttonX
|
||||
result[.y] = pad.buttonY
|
||||
}
|
||||
|
||||
// Shoulders & triggers
|
||||
result[.leftShoulder] = pad.leftShoulder
|
||||
result[.rightShoulder] = pad.rightShoulder
|
||||
result[.leftTrigger] = pad.leftTrigger
|
||||
result[.rightTrigger] = pad.rightTrigger
|
||||
|
||||
// Stick buttons
|
||||
if #available(iOS 12.1, *) {
|
||||
result[.leftStickButton] = pad.leftThumbstickButton
|
||||
result[.rightStickButton] = pad.rightThumbstickButton
|
||||
}
|
||||
|
||||
// DPad
|
||||
result[.dpadUp] = pad.dpad.up
|
||||
result[.dpadDown] = pad.dpad.down
|
||||
result[.dpadLeft] = pad.dpad.left
|
||||
result[.dpadRight] = pad.dpad.right
|
||||
|
||||
// Menu / Options / Share
|
||||
if #available(iOS 13.0, *) {
|
||||
result[.menu] = pad.buttonMenu
|
||||
if let options = pad.buttonOptions { result[.back] = options }
|
||||
}
|
||||
|
||||
if #available(iOS 14.0, tvOS 14.0, *) {
|
||||
if let home = pad.buttonHome {
|
||||
result[.special] = home
|
||||
}
|
||||
|
||||
if let controller = pad.controller {
|
||||
let profile = controller.physicalInputProfile
|
||||
if let paddle1 = profile.buttons[GCInputXboxPaddleOne] {
|
||||
result[.paddle1] = paddle1
|
||||
}
|
||||
if let paddle2 = profile.buttons[GCInputXboxPaddleTwo] {
|
||||
result[.paddle2] = paddle2
|
||||
}
|
||||
if let paddle3 = profile.buttons[GCInputXboxPaddleThree] {
|
||||
result[.paddle3] = paddle3
|
||||
}
|
||||
if let paddle4 = profile.buttons[GCInputXboxPaddleFour] {
|
||||
result[.paddle4] = paddle4
|
||||
}
|
||||
if let touchpadBtn = profile.buttons[GCInputDualShockTouchpadButton] {
|
||||
result[.touchpadButton] = touchpadBtn
|
||||
}
|
||||
if #available(iOS 15.0, tvOS 15.0, *) {
|
||||
if let share = profile.buttons[GCInputButtonShare] {
|
||||
result[.misc] = share
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@objc static func string(for button: ControllerButton) -> String {
|
||||
switch button {
|
||||
case .a: return "A"
|
||||
case .b: return "B"
|
||||
case .x: return "X"
|
||||
case .y: return "Y"
|
||||
|
||||
case .dpadUp: return SwiftLocalizationHelper.localizedString(forKey: "Up")
|
||||
case .dpadDown: return SwiftLocalizationHelper.localizedString(forKey: "Down")
|
||||
case .dpadLeft: return SwiftLocalizationHelper.localizedString(forKey: "Left")
|
||||
case .dpadRight: return SwiftLocalizationHelper.localizedString(forKey: "Right")
|
||||
|
||||
case .leftShoulder: return "LB"
|
||||
case .rightShoulder: return "RB"
|
||||
|
||||
case .leftStickButton: return "LS"
|
||||
case .rightStickButton: return "RS"
|
||||
|
||||
case .menu: return "Menu"
|
||||
case .back: return "Back"
|
||||
case .special: return "Home"
|
||||
|
||||
case .paddle1: return SwiftLocalizationHelper.localizedString(forKey: "Paddle1")
|
||||
case .paddle2: return SwiftLocalizationHelper.localizedString(forKey: "Paddle2")
|
||||
case .paddle3: return SwiftLocalizationHelper.localizedString(forKey: "Paddle3")
|
||||
case .paddle4: return SwiftLocalizationHelper.localizedString(forKey: "Paddle4")
|
||||
case .touchpadButton: return SwiftLocalizationHelper.localizedString(forKey: "Touch button")
|
||||
case .misc: return "Misc"
|
||||
|
||||
case .leftTrigger: return "LT"
|
||||
case .rightTrigger: return "RT"
|
||||
|
||||
case .null: return SwiftLocalizationHelper.localizedString(forKey: "Null")
|
||||
|
||||
default: return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
@objc static func compensated(offsetVector: CGVector, minOffset: CGFloat, circulate:Bool=false) -> CGVector{
|
||||
let vectorHypot = hypot(offsetVector.dx, offsetVector.dy)
|
||||
guard vectorHypot > 0 else {return CGVector(dx: 0, dy: 0)}
|
||||
let targetHypot = minOffset + (stickMaxOffset-minOffset)*(vectorHypot/stickMaxOffset)
|
||||
var compensatedX = targetHypot * (offsetVector.dx/vectorHypot)
|
||||
var compensatedY = targetHypot * (offsetVector.dy/vectorHypot)
|
||||
|
||||
if circulate {
|
||||
return circulated(offsetVector: CGVector(dx: compensatedX, dy: compensatedY))
|
||||
}
|
||||
else {
|
||||
compensatedX = max(min(compensatedX, stickMaxOffset),-stickMaxOffset)
|
||||
compensatedY = max(min(compensatedY, stickMaxOffset),-stickMaxOffset)
|
||||
return CGVector(dx: compensatedX, dy: compensatedY)
|
||||
}
|
||||
}
|
||||
|
||||
@objc static func circulated(offsetVector: CGVector) -> CGVector{
|
||||
let vectorHypot = hypot(offsetVector.dx, offsetVector.dy)
|
||||
guard vectorHypot > 0 else {return CGVector(dx: 0, dy: 0)}
|
||||
let targetHypot = min(vectorHypot, stickMaxOffset)
|
||||
let circulatedX = targetHypot*(offsetVector.dx/vectorHypot)
|
||||
let circulatedY = targetHypot*(offsetVector.dy/vectorHypot)
|
||||
return CGVector(dx: circulatedX, dy: circulatedY)
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property UIView* _view;
|
||||
@property NSMutableArray *layoutChanges;
|
||||
@property CALayer *layerBeingDragged;
|
||||
@property UIViewController *layoutToolVC;
|
||||
@property (nonatomic, weak) UIViewController *layoutToolVC;
|
||||
|
||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport
|
||||
streamConfig:(StreamConfiguration*)streamConfig oscLevel:(int)oscLevel;
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
layoutChanges = [[NSMutableArray alloc] init]; // will contain OSC button layout changes the user has made for this profile
|
||||
|
||||
[self drawButtons];
|
||||
// [self drawButtons];
|
||||
[self drawGuidelines]; // add the blue guidelines that appear when button is being tapped and dragged
|
||||
|
||||
return self;
|
||||
@@ -55,34 +55,44 @@
|
||||
/**
|
||||
* This method overrides the superclass's drawButtons method. The purpose of this method is to create a dPad parent layer, and add the four dPad buttons to it so that the user can drag the entire dPad around, directional buttons and all, as one unit as is the expected behavior. Note that we do not want the four dPad buttons to be child layers of a CALayer parent layer on the game stream view since the touch logic implemented for the four dPad buttons on the game stream view is written assuming the dPad buttons are not children of another parent CALayer
|
||||
*/
|
||||
- (void) drawButtons {
|
||||
[super setDPadCenter]; // Set custom position for D-Pad here
|
||||
[super setAnalogStickPositions]; // Set custom position for analog sticks here
|
||||
- (void) drawButtons:(OSCProfile* )profile {
|
||||
// [super regenLayerPool];
|
||||
[super setDPadCenter:profile]; // Set custom position for D-Pad here
|
||||
[super setAnalogStickPositions:profile]; // Set custom position for analog sticks here
|
||||
[super drawBumpers];
|
||||
[super drawButtons];
|
||||
[super drawButtons:profile];
|
||||
|
||||
/*
|
||||
UIImage* downButtonImage = [UIImage imageNamed:@"DownButton"];
|
||||
UIImage* rightButtonImage = [UIImage imageNamed:@"RightButton"];
|
||||
UIImage* upButtonImage = [UIImage imageNamed:@"UpButton"];
|
||||
UIImage* leftButtonImage = [UIImage imageNamed:@"LeftButton"];
|
||||
*/
|
||||
|
||||
UIImage* downButtonImage = [UIImage imageNamed:@"DownButton"];
|
||||
UIImage* rightButtonImage = [UIImage imageNamed:@"RightButton"];
|
||||
UIImage* upButtonImage = [UIImage imageNamed:@"UpButton"];
|
||||
UIImage* leftButtonImage = [UIImage imageNamed:@"LeftButton"];
|
||||
|
||||
// create dPad background layer
|
||||
self._dPadBackground = [CALayer layer];
|
||||
self._dPadBackground.name = @"dPad";
|
||||
self._dPadBackground.frame = CGRectMake(self.D_PAD_CENTER_X,
|
||||
self.D_PAD_CENTER_Y,
|
||||
self._leftButton.frame.size.width * 2 + BUTTON_DIST,
|
||||
self._leftButton.frame.size.width * 2 + BUTTON_DIST);
|
||||
self._dPadBackground.position = CGPointMake(self.D_PAD_CENTER_X, self.D_PAD_CENTER_Y); // since dPadBackground's dimensions have change after settings its width and height you need to reset its position again here
|
||||
[self.OSCButtonLayers addObject:self._dPadBackground];
|
||||
[_view.layer addSublayer:self._dPadBackground];
|
||||
if([self.OSCButtonLayerPool containsObject:self._upButton]){
|
||||
[self.OSCButtonLayerPool addObject:self._dPadBackground];
|
||||
self._dPadBackground.name = @"dPad";
|
||||
self._dPadBackground.frame = CGRectMake(self.D_PAD_CENTER_X,
|
||||
self.D_PAD_CENTER_Y,
|
||||
self._leftButton.frame.size.width * 2 + BUTTON_DIST,
|
||||
self._leftButton.frame.size.width * 2 + BUTTON_DIST);
|
||||
self._dPadBackground.position = CGPointMake(self.D_PAD_CENTER_X, self.D_PAD_CENTER_Y); // since dPadBackground's dimensions have change after settings its width and height you need to reset its position again here
|
||||
[self.OSCButtonLayerPool addObject:self._dPadBackground];
|
||||
[_view.layer addSublayer:self._dPadBackground];
|
||||
|
||||
// add dPad buttons to parent layer
|
||||
[self._dPadBackground addSublayer:self._downButton];
|
||||
[self._dPadBackground addSublayer:self._rightButton];
|
||||
[self._dPadBackground addSublayer:self._upButton];
|
||||
[self._dPadBackground addSublayer:self._leftButton];
|
||||
// add dPad buttons to parent layer
|
||||
[self._dPadBackground addSublayer:self._downButton];
|
||||
[self._dPadBackground addSublayer:self._rightButton];
|
||||
[self._dPadBackground addSublayer:self._upButton];
|
||||
[self._dPadBackground addSublayer:self._leftButton];
|
||||
}
|
||||
|
||||
NSLog(@"layer name ttttttttttt============================");
|
||||
for(CALayer* buttonLayer in self.OSCButtonLayerPool){
|
||||
NSLog(@"layer name %@", buttonLayer.name);
|
||||
}
|
||||
|
||||
[self resizeControllerLayerWith:self._dPadBackground and:_dPadSizeFactor];
|
||||
|
||||
@@ -128,7 +138,7 @@
|
||||
*/
|
||||
|
||||
horizontalGuideline.backgroundColor = [UIColor blueColor]; // change horizontal guideline back to blue if it doesn't line up with one of the on screen buttons
|
||||
for (CALayer *button in self.OSCButtonLayers) { // horizontal guideline position check
|
||||
for (CALayer *button in self.OSCButtonLayerPool) { // horizontal guideline position check
|
||||
if ((layerBeingDragged != button) && !button.isHidden) {
|
||||
if ((horizontalGuideline.center.y < button.position.y + 1) &&
|
||||
(horizontalGuideline.center.y > button.position.y - 1)) {
|
||||
@@ -147,7 +157,7 @@
|
||||
}
|
||||
|
||||
verticalGuideline.backgroundColor = [UIColor blueColor]; // change vertical guideline back to blue if it doesn't line up with one of the on screen buttons
|
||||
for (CALayer *button in self.OSCButtonLayers) { // vertical guideline position check
|
||||
for (CALayer *button in self.OSCButtonLayerPool) { // vertical guideline position check
|
||||
if ((layerBeingDragged != button) && !button.isHidden) {
|
||||
if ((verticalGuideline.center.x < button.position.x + 1) &&
|
||||
(verticalGuideline.center.x > button.position.x - 1)) {
|
||||
@@ -167,7 +177,7 @@
|
||||
|
||||
}
|
||||
|
||||
- (void) updateGuidelinesForLegacyOscButton{
|
||||
- (void) updateGuidelinesForLegacyOnScreenControls{
|
||||
/* have guidelines follow wherever the user is touching on the screen */
|
||||
horizontalGuideline.center = layerBeingDragged.position;
|
||||
verticalGuideline.center = layerBeingDragged.position;
|
||||
@@ -177,7 +187,7 @@
|
||||
-Change vertical guideline color to yellow if its x-position is almost equal to that of one of the buttons on screen.
|
||||
*/
|
||||
horizontalGuideline.backgroundColor = [UIColor blueColor]; // change horizontal guideline back to blue if it doesn't line up with one of the on screen buttons
|
||||
for (CALayer *button in self.OSCButtonLayers) { // horizontal guideline position check
|
||||
for (CALayer *button in self.OSCButtonLayerPool) { // horizontal guideline position check
|
||||
if ((layerBeingDragged != button) && !button.isHidden) {
|
||||
if ((horizontalGuideline.center.y < button.position.y + 1) &&
|
||||
(horizontalGuideline.center.y > button.position.y - 1)) {
|
||||
@@ -196,7 +206,7 @@
|
||||
}
|
||||
}
|
||||
verticalGuideline.backgroundColor = [UIColor blueColor]; // change vertical guideline back to blue if it doesn't line up with one of the on screen buttons
|
||||
for (CALayer *button in self.OSCButtonLayers) { // vertical guideline position check
|
||||
for (CALayer *button in self.OSCButtonLayerPool) { // vertical guideline position check
|
||||
if ((layerBeingDragged != button) && !button.isHidden) {
|
||||
if ((verticalGuideline.center.x < button.position.x + 1) &&
|
||||
(verticalGuideline.center.x > button.position.x - 1)) {
|
||||
@@ -233,7 +243,7 @@
|
||||
|
||||
/* returns reference to button layer object given the button's name*/
|
||||
- (CALayer*)controllerLayerFromName: (NSString*)name {
|
||||
for (CALayer *buttonLayer in self.OSCButtonLayers) {
|
||||
for (CALayer *buttonLayer in self.OSCButtonLayerPool) {
|
||||
|
||||
if ([buttonLayer.name isEqualToString:name]) {
|
||||
return buttonLayer;
|
||||
@@ -297,7 +307,7 @@
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"LegacyOscCALayerSelectedNotification" object:layerBeingDragged]; // inform that a CALayer of onScreen controller has being selected
|
||||
|
||||
/* save the name, position, and visibility of button being touched in array in case user wants to undo the change later */
|
||||
OnScreenButtonState *onScreenButtonState = [[OnScreenButtonState alloc] initWithButtonName:layerBeingDragged.name buttonType:LegacyOscButton andPosition:layerBeingDragged.position];
|
||||
OnScreenButtonState *onScreenButtonState = [[OnScreenButtonState alloc] initWithButtonName:layerBeingDragged.name buttonType:LegacyOnScreenControls andPosition:layerBeingDragged.position];
|
||||
// add hidden attr here
|
||||
onScreenButtonState.isHidden = layerBeingDragged.isHidden;
|
||||
|
||||
@@ -307,7 +317,7 @@
|
||||
/* make guide lines visible and position them over the button the user is touching */
|
||||
horizontalGuideline.hidden = NO;
|
||||
verticalGuideline.hidden = NO;
|
||||
[self updateGuidelinesForLegacyOscButton];
|
||||
[self updateGuidelinesForLegacyOnScreenControls];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +327,7 @@
|
||||
[CATransaction begin];
|
||||
[CATransaction setDisableActions:YES];
|
||||
layerBeingDragged.position = touchLocation; // move object to touch location
|
||||
[self updateGuidelinesForLegacyOscButton];
|
||||
[self updateGuidelinesForLegacyOnScreenControls];
|
||||
[CATransaction commit];
|
||||
}
|
||||
|
||||
|
||||
@@ -14,17 +14,63 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
#define DEFAULT_TEMPLATE_NAME1 @"控件模板 / Widget templates"
|
||||
#define DEFAULT_TEMPLATE_NAME2 @"旧版屏显控件 / Legacy on-screen controls"
|
||||
#define DEFAULT_TEMPLATE_NAME2 @"旧版控件 / Legacy widgets"
|
||||
|
||||
typedef NS_ENUM(NSInteger, MapGyroTo) {
|
||||
mapGyroToMouse,
|
||||
mapGyroToControllerStick,
|
||||
driftCorrection
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, PencilHoverMode) {
|
||||
HoverDisabled,
|
||||
HoverPencil,
|
||||
HoverMouse,
|
||||
HoverBoth
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
This object contains information pertaining to any of the user created, custom on screen controller layout configurations, or 'profiles.' The object contains a 'name' property for easy reference, as well as an 'isSelected' property which is used to determine whether this particular custom OSC layout should show on screen during game stream view. Only one 'OSCProfile' is set to 'isSelected' at any given time. The object also contains an array of 'OnScreenButtonStates' which provides information that allows us to move and hide/unhide each of the 19 on screen buttons. Note that the 'buttonStates' property should contain an NSMutableArray of ENCODED 'OnScreenButtonState' objects. This allows us to save the 'OSCProfile' object to NSUserDefaults.
|
||||
Additionally the 'OSCProfile' object adopts encoding and decoding protocols so that we can encode the object before saving it to NSUserDefaults. By saving this object to NSUserDefaults we allow the user to save and load their custom on screen controller button layouts between app launches
|
||||
*/
|
||||
@interface OSCProfile : NSObject <NSCoding, NSSecureCoding>
|
||||
@interface OSCProfile : NSObject <NSCoding, NSSecureCoding, NSMutableCopying>
|
||||
|
||||
@property NSString *name;
|
||||
@property NSMutableArray <OnScreenButtonState *> *buttonStates;
|
||||
@property NSMutableArray <NSData *> *buttonStatesEncoded;
|
||||
@property (nonatomic, assign) int16_t unfoldedExclusiveFolderSequence;
|
||||
@property NSSet<NSNumber *> *postExclusiveUnfoldedSequences;
|
||||
@property BOOL isSelected;
|
||||
@property (nonatomic, assign) MapGyroTo mapGyroTo;
|
||||
@property (nonatomic, assign) bool yawPitchToRightStick;
|
||||
@property (nonatomic, assign) bool rollToLeftStick;
|
||||
@property (nonatomic, assign) bool synthesizePhysicalStick;
|
||||
@property (nonatomic, assign) CGFloat gyroSensitivityYaw;
|
||||
@property (nonatomic, assign) CGFloat gyroSensitivityPitch;
|
||||
@property (nonatomic, assign) CGFloat gyroSensitivityRoll;
|
||||
@property (nonatomic, assign) CGFloat accelSensitivityX;
|
||||
@property (nonatomic, assign) CGFloat accelSensitivityY;
|
||||
@property (nonatomic, assign) CGFloat accelSensitivityZ;
|
||||
@property (nonatomic, assign) double gyroToStickMinOffset;
|
||||
@property (nonatomic, assign) double physicalLeftStickMinOffset;
|
||||
@property (nonatomic, assign) double physicalRightStickMinOffset;
|
||||
@property (nonatomic, assign) int controllerGyroSwitchMode;
|
||||
@property (nonatomic, assign) bool reverseGyroHoldButton;
|
||||
@property (nonatomic, assign) int controllerGyroSwitchHold;
|
||||
@property (nonatomic, assign) int controllerGyroSwitchToggle;
|
||||
@property NSArray<NSNumber *> *pressureCurvePoints;
|
||||
@property (nonatomic, assign) bool pressureCurveEnabled;
|
||||
@property (nonatomic, assign) bool doubleTapShorcutEnabled;
|
||||
@property NSString *brushShortcut;
|
||||
@property NSString *eraserShortcut;
|
||||
@property (nonatomic, assign) bool squeezeShorcutEnabled;
|
||||
@property NSString *squeezeStartShortcut;
|
||||
@property NSString *squeezeEndShortcut;
|
||||
@property (nonatomic, assign) bool pencilPausesNativeTouch;
|
||||
@property (nonatomic, assign) bool disablePencilSlideGestures;
|
||||
@property (nonatomic, assign) PencilHoverMode pencilHoverMode;
|
||||
|
||||
|
||||
|
||||
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected;
|
||||
|
||||
|
||||
@@ -10,13 +10,14 @@
|
||||
//
|
||||
|
||||
#import "OSCProfile.h"
|
||||
#import "VoidLink-Swift.h"
|
||||
|
||||
@implementation OSCProfile
|
||||
|
||||
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected {
|
||||
if ((self = [self init])) {
|
||||
self.name = name;
|
||||
self.buttonStates = buttonStates;
|
||||
self.buttonStatesEncoded = buttonStates;
|
||||
self.isSelected = isSelected;
|
||||
}
|
||||
|
||||
@@ -29,18 +30,177 @@
|
||||
|
||||
- (void) encodeWithCoder:(NSCoder*)encoder {
|
||||
[encoder encodeObject:self.name forKey:@"name"];
|
||||
[encoder encodeObject:self.buttonStates forKey:@"buttonStates"];
|
||||
[encoder encodeObject:self.buttonStatesEncoded forKey:@"buttonStates"];
|
||||
[encoder encodeInt32:self.unfoldedExclusiveFolderSequence forKey:@"unfoldedExclusiveFolderSequence"];
|
||||
[encoder encodeObject:self.postExclusiveUnfoldedSequences forKey:@"postExclusiveUnfoldedSequences"];
|
||||
[encoder encodeBool:self.isSelected forKey:@"isSelected"];
|
||||
[encoder encodeInt64:self.mapGyroTo forKey:@"mapGyroTo"];
|
||||
[encoder encodeBool:self.yawPitchToRightStick forKey:@"yawPitchToRightStick"];
|
||||
[encoder encodeBool:self.rollToLeftStick forKey:@"rollToLeftStick"];
|
||||
[encoder encodeBool:self.synthesizePhysicalStick forKey:@"synthesizePhysicalStick"];
|
||||
[encoder encodeFloat:self.gyroSensitivityYaw forKey:@"gyroSensitivityYaw"];
|
||||
[encoder encodeFloat:self.gyroSensitivityPitch forKey:@"gyroSensitivityPitch"];
|
||||
[encoder encodeFloat:self.gyroSensitivityRoll forKey:@"gyroSensitivityRoll"];
|
||||
[encoder encodeFloat:self.accelSensitivityX forKey:@"accelSensitivityX"];
|
||||
[encoder encodeFloat:self.accelSensitivityY forKey:@"accelSensitivityY"];
|
||||
[encoder encodeFloat:self.accelSensitivityZ forKey:@"accelSensitivityZ"];
|
||||
[encoder encodeDouble:self.gyroToStickMinOffset forKey:@"gyroToStickMinOffset"];
|
||||
[encoder encodeDouble:self.physicalLeftStickMinOffset forKey:@"physicalLeftStickMinOffset"];
|
||||
[encoder encodeDouble:self.physicalRightStickMinOffset forKey:@"physicalRightStickMinOffset"];
|
||||
[encoder encodeInt:self.controllerGyroSwitchMode forKey:@"controllerGyroSwitchMode"];
|
||||
[encoder encodeBool:self.reverseGyroHoldButton forKey:@"reverseGyroHoldButton"];
|
||||
[encoder encodeInt:self.controllerGyroSwitchHold forKey:@"controllerGyroSwitchHold"];
|
||||
[encoder encodeInt:self.controllerGyroSwitchToggle forKey:@"controllerGyroSwitchToggle"];
|
||||
[encoder encodeObject:self.pressureCurvePoints forKey:@"pressureCurvePoints"];
|
||||
[encoder encodeBool:self.pressureCurveEnabled forKey:@"pressureCurveEnabled"];
|
||||
[encoder encodeBool:self.doubleTapShorcutEnabled forKey:@"doubleTapShorcutEnabled"];
|
||||
[encoder encodeObject:self.brushShortcut forKey:@"brushShortcut"];
|
||||
[encoder encodeObject:self.eraserShortcut forKey:@"eraserShortcut"];
|
||||
[encoder encodeBool:self.squeezeShorcutEnabled forKey:@"squeezeShorcutEnabled"];
|
||||
[encoder encodeObject:self.squeezeStartShortcut forKey:@"squeezeStartShortcut"];
|
||||
[encoder encodeObject:self.squeezeEndShortcut forKey:@"squeezeEndShortcut"];
|
||||
[encoder encodeBool:self.pencilPausesNativeTouch forKey:@"pencilPausesNativeTouch"];
|
||||
[encoder encodeBool:self.disablePencilSlideGestures forKey:@"disablePencilSlideGestures"];
|
||||
[encoder encodeInt64:self.pencilHoverMode forKey:@"pencilHoverMode"];
|
||||
}
|
||||
|
||||
- (id) initWithCoder:(NSCoder*)decoder {
|
||||
if (self = [super init]) {
|
||||
self.name = [decoder decodeObjectForKey:@"name"];
|
||||
self.buttonStates = [decoder decodeObjectForKey:@"buttonStates"];
|
||||
self.name =
|
||||
[decoder decodeObjectOfClasses:
|
||||
[NSSet setWithObjects:
|
||||
[NSString class],
|
||||
nil]
|
||||
forKey:@"name"];
|
||||
self.buttonStatesEncoded =
|
||||
[decoder decodeObjectOfClasses:
|
||||
[NSSet setWithObjects:
|
||||
[NSMutableArray class],
|
||||
[NSArray class],
|
||||
[NSData class],
|
||||
nil]
|
||||
forKey:@"buttonStates"];
|
||||
self.unfoldedExclusiveFolderSequence = [decoder containsValueForKey:@"unfoldedExclusiveFolderSequence"] ? [decoder decodeInt32ForKey:@"unfoldedExclusiveFolderSequence"] : -1;
|
||||
self.postExclusiveUnfoldedSequences = [decoder containsValueForKey:@"postExclusiveUnfoldedSequences"] ?
|
||||
[decoder decodeObjectOfClasses:
|
||||
[NSSet setWithObjects:
|
||||
[NSMutableSet class],
|
||||
[NSSet class],
|
||||
[NSNumber class],
|
||||
nil]
|
||||
forKey:@"postExclusiveUnfoldedSequences"] :
|
||||
[NSSet set];
|
||||
self.isSelected = [decoder decodeBoolForKey:@"isSelected"];
|
||||
self.mapGyroTo = [decoder containsValueForKey:@"mapGyroTo"] ? [decoder decodeInt64ForKey:@"mapGyroTo"] : mapGyroToMouse;
|
||||
self.yawPitchToRightStick = [decoder containsValueForKey:@"yawPitchToRightStick"] ? [decoder decodeBoolForKey:@"yawPitchToRightStick"] : true;
|
||||
self.rollToLeftStick = [decoder containsValueForKey:@"rollToLeftStick"] ? [decoder decodeBoolForKey:@"rollToLeftStick"] : false;
|
||||
self.synthesizePhysicalStick = [decoder containsValueForKey:@"synthesizePhysicalStick"] ? [decoder decodeBoolForKey:@"synthesizePhysicalStick"] : true;
|
||||
self.gyroSensitivityYaw = [decoder containsValueForKey:@"gyroSensitivityYaw"] ? [decoder decodeFloatForKey:@"gyroSensitivityYaw"] : 1.0;
|
||||
self.gyroSensitivityPitch = [decoder containsValueForKey:@"gyroSensitivityPitch"] ? [decoder decodeFloatForKey:@"gyroSensitivityPitch"] : 1.0;
|
||||
self.gyroSensitivityRoll = [decoder containsValueForKey:@"gyroSensitivityRoll"] ? [decoder decodeFloatForKey:@"gyroSensitivityRoll"] : 1.0;
|
||||
self.accelSensitivityX = [decoder containsValueForKey:@"accelSensitivityX"] ? [decoder decodeFloatForKey:@"accelSensitivityX"] : 1.0;
|
||||
self.accelSensitivityY = [decoder containsValueForKey:@"accelSensitivityY"] ? [decoder decodeFloatForKey:@"accelSensitivityY"] : 1.0;
|
||||
self.accelSensitivityZ = [decoder containsValueForKey:@"accelSensitivityZ"] ? [decoder decodeFloatForKey:@"accelSensitivityZ"] : 1.0;
|
||||
self.gyroToStickMinOffset = [decoder containsValueForKey:@"gyroToStickMinOffset"] ? [decoder decodeDoubleForKey:@"gyroToStickMinOffset"] : 0;
|
||||
self.physicalLeftStickMinOffset = [decoder containsValueForKey:@"physicalLeftStickMinOffset"] ? [decoder decodeDoubleForKey:@"physicalLeftStickMinOffset"] : 0;
|
||||
self.physicalRightStickMinOffset = [decoder containsValueForKey:@"physicalRightStickMinOffset"] ? [decoder decodeDoubleForKey:@"physicalRightStickMinOffset"] : 0;
|
||||
self.controllerGyroSwitchMode = [decoder containsValueForKey:@"controllerGyroSwitchMode"] ? [decoder decodeIntForKey:@"controllerGyroSwitchMode"] : ControllerGyroSwitchDisabled;
|
||||
self.reverseGyroHoldButton = [decoder containsValueForKey:@"reverseGyroHoldButton"] ? [decoder decodeBoolForKey:@"reverseGyroHoldButton"] : false;
|
||||
self.controllerGyroSwitchHold = [decoder containsValueForKey:@"controllerGyroSwitchHold"] ? [decoder decodeIntForKey:@"controllerGyroSwitchHold"] : ControllerButtonNull;
|
||||
self.controllerGyroSwitchToggle = [decoder containsValueForKey:@"controllerGyroSwitchToggle"] ? [decoder decodeIntForKey:@"controllerGyroSwitchToggle"] : ControllerButtonNull;
|
||||
self.pressureCurvePoints =
|
||||
[decoder containsValueForKey:@"pressureCurvePoints"]
|
||||
? [decoder decodeObjectOfClasses:
|
||||
[NSSet setWithObjects:
|
||||
[NSArray class],
|
||||
[NSNumber class],
|
||||
nil]
|
||||
forKey:@"pressureCurvePoints"]
|
||||
: @[@0.0, @0.0, @1.0, @1.0];
|
||||
self.pressureCurveEnabled = [decoder containsValueForKey:@"pressureCurveEnabled"] ? [decoder decodeBoolForKey:@"pressureCurveEnabled"] : false;
|
||||
|
||||
self.doubleTapShorcutEnabled = [decoder containsValueForKey:@"doubleTapShorcutEnabled"] ? [decoder decodeBoolForKey:@"doubleTapShorcutEnabled"] : false;
|
||||
self.brushShortcut =
|
||||
[decoder containsValueForKey:@"brushShortcut"]
|
||||
? [decoder decodeObjectOfClasses:
|
||||
[NSSet setWithObjects:
|
||||
[NSString class],
|
||||
nil]
|
||||
forKey:@"brushShortcut"]
|
||||
: @"";
|
||||
|
||||
self.eraserShortcut =
|
||||
[decoder containsValueForKey:@"eraserShortcut"]
|
||||
? [decoder decodeObjectOfClasses:
|
||||
[NSSet setWithObjects:
|
||||
[NSString class],
|
||||
nil]
|
||||
forKey:@"eraserShortcut"]
|
||||
: @"";
|
||||
|
||||
self.squeezeShorcutEnabled = [decoder containsValueForKey:@"squeezeShorcutEnabled"] ? [decoder decodeBoolForKey:@"squeezeShorcutEnabled"] : false;
|
||||
self.squeezeStartShortcut =
|
||||
[decoder containsValueForKey:@"squeezeStartShortcut"]
|
||||
? [decoder decodeObjectOfClasses:
|
||||
[NSSet setWithObjects:
|
||||
[NSString class],
|
||||
nil]
|
||||
forKey:@"squeezeStartShortcut"]
|
||||
: @"";
|
||||
|
||||
self.squeezeEndShortcut =
|
||||
[decoder containsValueForKey:@"squeezeEndShortcut"]
|
||||
? [decoder decodeObjectOfClasses:
|
||||
[NSSet setWithObjects:
|
||||
[NSString class],
|
||||
nil]
|
||||
forKey:@"squeezeEndShortcut"]
|
||||
: @"";
|
||||
|
||||
self.pencilPausesNativeTouch = [decoder containsValueForKey:@"pencilPausesNativeTouch"] ? [decoder decodeBoolForKey:@"pencilPausesNativeTouch"] : false;
|
||||
self.disablePencilSlideGestures = [decoder containsValueForKey:@"disablePencilSlideGestures"] ? [decoder decodeBoolForKey:@"disablePencilSlideGestures"] : false;
|
||||
self.pencilHoverMode = [decoder containsValueForKey:@"pencilHoverMode"] ? [decoder decodeInt64ForKey:@"pencilHoverMode"] : HoverPencil;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nonnull id)mutableCopyWithZone:(nullable NSZone *)zone {
|
||||
OSCProfile *copy = [[[self class] allocWithZone:zone] init];
|
||||
copy.name = [self.name mutableCopy]; // NSString → NSMutableString
|
||||
copy.buttonStatesEncoded = [[NSMutableArray alloc] initWithArray:self.buttonStatesEncoded copyItems:YES];
|
||||
copy.unfoldedExclusiveFolderSequence = self.unfoldedExclusiveFolderSequence;
|
||||
copy.postExclusiveUnfoldedSequences = [self.postExclusiveUnfoldedSequences copy];
|
||||
copy.isSelected = self.isSelected;
|
||||
copy.mapGyroTo = self.mapGyroTo;
|
||||
copy.yawPitchToRightStick = self.yawPitchToRightStick;
|
||||
copy.rollToLeftStick = self.rollToLeftStick;
|
||||
copy.synthesizePhysicalStick = self.synthesizePhysicalStick;
|
||||
copy.gyroSensitivityYaw = self.gyroSensitivityYaw;
|
||||
copy.gyroSensitivityPitch = self.gyroSensitivityPitch;
|
||||
copy.gyroSensitivityRoll = self.gyroSensitivityRoll;
|
||||
copy.accelSensitivityX = self.accelSensitivityX;
|
||||
copy.accelSensitivityY = self.accelSensitivityY;
|
||||
copy.accelSensitivityZ = self.accelSensitivityZ;
|
||||
copy.gyroToStickMinOffset = self.gyroToStickMinOffset;
|
||||
copy.physicalLeftStickMinOffset = self.physicalLeftStickMinOffset;
|
||||
copy.physicalRightStickMinOffset = self.physicalRightStickMinOffset;
|
||||
copy.controllerGyroSwitchMode = self.controllerGyroSwitchMode;
|
||||
copy.reverseGyroHoldButton = self.reverseGyroHoldButton;
|
||||
copy.controllerGyroSwitchHold = self.controllerGyroSwitchHold;
|
||||
copy.controllerGyroSwitchToggle = self.controllerGyroSwitchToggle;
|
||||
copy.pressureCurvePoints = [[NSMutableArray alloc] initWithArray:self.pressureCurvePoints copyItems:YES];
|
||||
copy.pressureCurveEnabled = self.pressureCurveEnabled;
|
||||
copy.doubleTapShorcutEnabled = self.doubleTapShorcutEnabled;
|
||||
copy.brushShortcut = [self.brushShortcut mutableCopy]; // NSString → NSMutableString
|
||||
copy.eraserShortcut = [self.eraserShortcut mutableCopy]; // NSString → NSMutableString
|
||||
copy.squeezeShorcutEnabled = self.squeezeShorcutEnabled;
|
||||
copy.squeezeStartShortcut = [self.squeezeStartShortcut mutableCopy]; // NSString → NSMutableString
|
||||
copy.squeezeEndShortcut = [self.squeezeEndShortcut mutableCopy]; // NSString → NSMutableString
|
||||
copy.pencilPausesNativeTouch = self.pencilPausesNativeTouch;
|
||||
copy.disablePencilSlideGestures = self.disablePencilSlideGestures;
|
||||
copy.pencilHoverMode = self.pencilHoverMode;
|
||||
return copy;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OSCProfile.h"
|
||||
#import "DataManager.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -20,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
*/
|
||||
@interface OSCProfilesManager : NSObject
|
||||
@property NSMutableArray <OSCProfile *> *currentProfiles;
|
||||
|
||||
@property (atomic, assign) WidgetSizeTransition widgetSizeTransition;
|
||||
|
||||
+ (OSCProfilesManager *)sharedManager:(CGRect)viewBounds;
|
||||
+ (void)setOnScreenWidgetViewsSet:(NSMutableSet* )set;
|
||||
@@ -38,28 +39,34 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
*/
|
||||
- (OSCProfile *) getSelectedProfile;
|
||||
|
||||
- (uint32_t) getIndexOfLastProfile;
|
||||
|
||||
/**
|
||||
* Returns the index of the 'selected' profile within the array it's in
|
||||
*/
|
||||
- (NSInteger) getIndexOfSelectedProfile;
|
||||
|
||||
- (NSMutableArray *) getEncodedProfiles;
|
||||
// - (NSData* )getSelectedEncodedProfile;
|
||||
- (void) importEncodedProfiles:(NSMutableArray* )profilesEncoded;
|
||||
- (OnScreenButtonState *)unarchiveButtonStateEncoded:(NSData *)data;
|
||||
- (void) updateDefaultTemplates;
|
||||
- (void) importDefaultTemplates;
|
||||
|
||||
#pragma mark - Setters
|
||||
/**
|
||||
* Sets the profile object with the particular 'name' as the selected profile to be displayed on screen during game streaming
|
||||
* Sets the profile object with the particular index as the selected profile to be displayed on screen during game streaming
|
||||
*/
|
||||
- (void) setProfileToSelected:(NSString *)name;
|
||||
- (void) setProfileToSelected:(uint32_t)index;
|
||||
- (void) replaceSelectedProfileWith:(OSCProfile*)newProfile overwriteDefault:(bool)overwriteDefault;
|
||||
- (void) replaceProfile:(OSCProfile*)oldProfile withProfile:(OSCProfile*)newProfile;
|
||||
|
||||
/**
|
||||
* Saves a profile object with a particular 'name' and an array of button layers (the CALayer button layers are the objects currently visible on screen) to persistent storage
|
||||
*/
|
||||
- (void) saveProfileWithName:(NSString*)name andButtonLayers:(NSMutableArray *)buttonLayers;
|
||||
- (void) duplicateSelectedProfileWithName:(NSString*)name;
|
||||
|
||||
- (bool) updateSelectedProfile:(NSMutableSet *) oscButtonLayers;
|
||||
|
||||
- (bool) updateSelectedProfile:(NSMutableArray *) oscButtonLayers;
|
||||
/**
|
||||
* Delete current selected profile.
|
||||
*/
|
||||
@@ -71,6 +78,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
*/
|
||||
- (BOOL) profileNameAlreadyExist:(NSString*)name;
|
||||
- (OSCProfile *) findProfileByName:(NSString*) name inProfileArray:(NSMutableArray*)profiles;
|
||||
- (CGPoint)normalizeWidgetPosition:(CGPoint)position;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ static CGRect layoutViewBounds;
|
||||
+ (OSCProfilesManager *) sharedManager:(CGRect)viewBounds {
|
||||
static OSCProfilesManager *_sharedManager = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
layoutViewBounds = viewBounds;
|
||||
if(!CGRectEqualToRect(viewBounds, CGRectZero)) layoutViewBounds = viewBounds;
|
||||
// NSLog(@"bounds width: %f, height: %f", layoutViewBounds.size.width, layoutViewBounds.size.height);
|
||||
dispatch_once(&onceToken, ^{
|
||||
_sharedManager = [[self alloc] init];
|
||||
@@ -105,6 +105,22 @@ static CGRect layoutViewBounds;
|
||||
|
||||
}
|
||||
|
||||
- (void)replaceSelectedProfileWith:(OSCProfile*)newProfile overwriteDefault:(bool)overwriteDefault{
|
||||
NSInteger index = 0;
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
for (OSCProfile *profile in profiles) {
|
||||
if (profile.isSelected == YES) {
|
||||
index = [profiles indexOfObject:profile];
|
||||
}
|
||||
}
|
||||
if(index>0) profiles[index] = newProfile;
|
||||
else if(overwriteDefault) profiles[index] = newProfile;
|
||||
|
||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles];
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces one 'OSCProfile' object for another in the 'OSCProfile' objects array stored in persistent storage
|
||||
@@ -139,18 +155,32 @@ static CGRect layoutViewBounds;
|
||||
}
|
||||
|
||||
- (NSMutableArray *) getEncodedProfiles {
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
|
||||
NSData *profilesArrayEncoded = [[NSUserDefaults standardUserDefaults] objectForKey: @"OSCProfiles"]; // Get the encoded array of encoded OSC profiles from persistent storage
|
||||
NSSet *classes = [NSSet setWithObjects:[NSString class], [NSMutableData class], [NSMutableArray class], [OSCProfile class], [OnScreenButtonState class], nil];
|
||||
|
||||
NSMutableArray *profilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:profilesArrayEncoded error:nil]; // Decode the encoded array itself, NOT the objects contained in the array
|
||||
|
||||
return profilesEncoded;
|
||||
}
|
||||
|
||||
- (void) importEncodedProfiles:(NSMutableArray* )profilesEncoded {
|
||||
NSMutableArray* targetProfiles = [_currentProfiles mutableCopy]; //_currentProfiles is availabled as long as getAllProfiles was called before calling this method (_currentProfiles avoids frequent accessing persisted data)
|
||||
OSCProfile* profile;
|
||||
/*
|
||||
profile = [self findProfileByName:DEFAULT_TEMPLATE_NAME1 inProfileArray:targetProfiles];
|
||||
if(profile && targetProfiles.count > 1) [targetProfiles removeObject:profile];
|
||||
profile = [self findProfileByName:DEFAULT_TEMPLATE_NAME2 inProfileArray:targetProfiles];
|
||||
if(profile && targetProfiles.count > 1) [targetProfiles removeObject:profile];
|
||||
*/
|
||||
|
||||
NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];
|
||||
bool importPencilProProfile = ([bundleId isEqualToString:@"com.voidlink.iOS"]
|
||||
|| [bundleId isEqualToString:@"com.voidlinkextreme.iOS"]
|
||||
|| [bundleId isEqualToString:@"com.voidlink.tf.debug10.iOS"]);
|
||||
|
||||
if(!importPencilProProfile && [GenericUtils isIPad]){
|
||||
[profilesEncoded removeObjectAtIndex:1];
|
||||
}
|
||||
|
||||
if(targetProfiles.count > 0) [targetProfiles removeObjectAtIndex:0];
|
||||
NSMutableArray* localEncodedPofiles = [self encodedProfilesFromArray:targetProfiles];
|
||||
[profilesEncoded addObjectsFromArray:localEncodedPofiles];
|
||||
@@ -164,7 +194,6 @@ static CGRect layoutViewBounds;
|
||||
return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
||||
}
|
||||
|
||||
|
||||
- (void) importDefaultTemplates{
|
||||
NSString *filePath = [[NSBundle mainBundle] pathForResource: [self isIPhone] ? @"widgetTemplatesIPhone": @"widgetTemplates" ofType:@"bin"];
|
||||
if (filePath) {
|
||||
@@ -177,6 +206,47 @@ static CGRect layoutViewBounds;
|
||||
NSSet *classes = [NSSet setWithObjects: [NSMutableData class], [NSMutableArray class], nil];
|
||||
profilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:fileData error:&error];
|
||||
[self importEncodedProfiles:profilesEncoded];
|
||||
[self setProfileToSelected:0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) updateDefaultTemplates{
|
||||
NSString *filePath = [[NSBundle mainBundle] pathForResource: [self isIPhone] ? @"widgetTemplatesIPhone": @"widgetTemplates" ofType:@"bin"];
|
||||
if (filePath) {
|
||||
// 2. 读取二进制数据
|
||||
NSError *error;
|
||||
NSData *fileData = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error];
|
||||
if (fileData && !error) {
|
||||
NSSet *classes = [NSSet setWithObjects:[NSString class], [NSMutableData class], [NSMutableArray class], [OSCProfile class], [OnScreenButtonState class], nil];
|
||||
NSMutableArray *defaultProfilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:fileData error:nil]; // Decode the encoded array itself, NOT the objects contained in the array
|
||||
OSCProfile *defaultProfileDecoded;
|
||||
NSMutableArray *targetProfilesDecoded = [self getAllProfiles];
|
||||
OSCProfile *newPencilProProfile;
|
||||
bool pencilProProfileExisted = false;
|
||||
for (NSData *profileEncoded in defaultProfilesEncoded) {
|
||||
defaultProfileDecoded = [NSKeyedUnarchiver unarchivedObjectOfClasses: classes fromData:profileEncoded error: nil];
|
||||
OSCProfile *targetProfile = [self findProfileByName:defaultProfileDecoded.name inProfileArray:targetProfilesDecoded];
|
||||
if([defaultProfileDecoded.name isEqualToString:@"Pencil Pro"]){
|
||||
newPencilProProfile = [defaultProfileDecoded mutableCopy];
|
||||
}
|
||||
if(targetProfile){
|
||||
if([targetProfile.name isEqualToString:@"Pencil Pro"]){
|
||||
pencilProProfileExisted = true;
|
||||
}
|
||||
uint64_t targetIndex = [targetProfilesDecoded indexOfObject:targetProfile];
|
||||
targetProfilesDecoded[targetIndex] = defaultProfileDecoded;
|
||||
}
|
||||
}
|
||||
if(newPencilProProfile && !pencilProProfileExisted){
|
||||
if(newPencilProProfile) [targetProfilesDecoded insertObject:newPencilProProfile atIndex:1];
|
||||
}
|
||||
|
||||
NSMutableArray* targetPofilesEncoded = [self encodedProfilesFromArray:targetProfilesDecoded];
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:targetPofilesEncoded requiringSecureCoding:YES error:nil];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
[self setProfileToSelected:0];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,7 +257,6 @@ static CGRect layoutViewBounds;
|
||||
#pragma mark - Getters
|
||||
|
||||
- (NSMutableArray *) getAllProfiles {
|
||||
|
||||
NSData *profilesArrayEncoded = [[NSUserDefaults standardUserDefaults] objectForKey: @"OSCProfiles"]; // Get the encoded array of encoded OSC profiles from persistent storage
|
||||
NSSet *classes = [NSSet setWithObjects:[NSString class], [NSMutableData class], [NSMutableArray class], [OSCProfile class], [OnScreenButtonState class], nil];
|
||||
|
||||
@@ -206,15 +275,27 @@ static CGRect layoutViewBounds;
|
||||
}
|
||||
|
||||
- (OSCProfile *) getSelectedProfile {
|
||||
NSLog(@"getAllProfiles test %f", CACurrentMediaTime());
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
|
||||
// NSString* persistedKey = @"widgetProfileUpdated-20251015";
|
||||
NSString* persistedKey = @"widgetProfileUpdated-20260213";
|
||||
BOOL needImportDefaultTemplates = [defaults objectForKey:persistedKey] == nil;
|
||||
|
||||
if(profiles.count == 0 || needImportDefaultTemplates){
|
||||
if(profiles.count == 0) [self importDefaultTemplates];
|
||||
else [self updateDefaultTemplates];
|
||||
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:persistedKey];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
profiles = [self getAllProfiles];
|
||||
}
|
||||
for (OSCProfile *profile in profiles) {
|
||||
|
||||
if (profile.isSelected) {
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
return profiles[0];
|
||||
}
|
||||
|
||||
- (NSInteger) getIndexOfSelectedProfile {
|
||||
@@ -228,22 +309,20 @@ static CGRect layoutViewBounds;
|
||||
return 0; // if none of the profiles in the array have their 'isSelected' property set to YES (which should not be possible) return the 'Default' profile as the 'selected' profile
|
||||
}
|
||||
|
||||
- (uint32_t) getIndexOfLastProfile {
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
return (uint32_t)profiles.count-1; // if none of the profiles in the array have their 'isSelected' property set to YES (which should not be possible) return the 'Default' profile as the 'selected' profile
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setters
|
||||
|
||||
- (void) setProfileToSelected:(NSString *)name {
|
||||
- (void) setProfileToSelected:(uint32_t)tableIndex {
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
|
||||
/* Iterate through each profile. If its name equals the value of the 'name' parameter passed into this method then set the profile's 'isSelected' property to YES, otherwise set the value to NO */
|
||||
for (OSCProfile *profile in profiles) {
|
||||
|
||||
if ([profile.name isEqualToString:name]) {
|
||||
profile.isSelected = YES;
|
||||
}
|
||||
else {
|
||||
profile.isSelected = NO;
|
||||
}
|
||||
}
|
||||
[profiles enumerateObjectsUsingBlock:^(OSCProfile* profile, NSUInteger idx, BOOL *stop) {
|
||||
profile.isSelected = idx == tableIndex;
|
||||
}];
|
||||
|
||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
|
||||
|
||||
@@ -254,55 +333,45 @@ static CGRect layoutViewBounds;
|
||||
}
|
||||
|
||||
|
||||
- (bool) updateSelectedProfile:(NSMutableArray *) oscButtonLayers {
|
||||
- (bool) updateSelectedProfile:(NSMutableSet *) oscButtonLayers {
|
||||
NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndWidgetsToButtonStates:oscButtonLayers];
|
||||
if([self getIndexOfSelectedProfile]==0) return false;
|
||||
/*
|
||||
OSCProfile *newProfile = [[OSCProfile alloc] initWithName:[self getSelectedProfile].name
|
||||
buttonStates:buttonStatesEncoded isSelected:YES]; // create a new 'OSCProfile'. Set the array of encoded button states created above to the 'buttonStates' property of the new profile, along with a 'name'. Set 'isSelected' argument to YES which will set this saved profile as the one that will show up in the game stream view
|
||||
|
||||
|
||||
/* set all saved OSCProfiles 'isSelected' property to NO since the new profile you're adding will be set as the selected profile */
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
for (OSCProfile *profile in profiles) {
|
||||
profile.isSelected = NO;
|
||||
}
|
||||
[self replaceProfile:[self getSelectedProfile] withProfile:newProfile];
|
||||
*/
|
||||
OSCProfile *selectedProfile = [self getSelectedProfile];
|
||||
selectedProfile.buttonStatesEncoded = buttonStatesEncoded;
|
||||
selectedProfile.unfoldedExclusiveFolderSequence = OnScreenWidgetView.unfoldedExclusiveFolderSequence;
|
||||
selectedProfile.postExclusiveUnfoldedSequences = OnScreenWidgetView.postExclusiveUnfoldedSequences;
|
||||
[self replaceSelectedProfileWith:selectedProfile overwriteDefault:NO];
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
- (void) saveProfileWithName:(NSString*)name andButtonLayers:(NSMutableArray *)oscButtonLayers {
|
||||
NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndWidgetsToButtonStates:oscButtonLayers];
|
||||
OSCProfile *newProfile = [[OSCProfile alloc] initWithName:name
|
||||
buttonStates:buttonStatesEncoded isSelected:YES]; // create a new 'OSCProfile'. Set the array of encoded button states created above to the 'buttonStates' property of the new profile, along with a 'name'. Set 'isSelected' argument to YES which will set this saved profile as the one that will show up in the game stream view
|
||||
/* set all saved OSCProfiles 'isSelected' property to NO since the new profile you're adding will be set as the selected profile */
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
for (OSCProfile *profile in profiles) {
|
||||
|
||||
profile.isSelected = NO;
|
||||
}
|
||||
|
||||
if ([self profileNameAlreadyExist:name]) { // if a saved profile with the same 'name' already exists in persistent storage then overwrite it and save the change to persistent storage
|
||||
[self replaceProfile:[self OSCProfileWithName:name] withProfile:newProfile];
|
||||
}
|
||||
- (void) duplicateSelectedProfileWithName:(NSString*)name {
|
||||
if ([self profileNameAlreadyExist:name]) return;
|
||||
else { // otherwise encode then add the new profile to the end of the OSCProfiles array
|
||||
NSData *newProfileEncoded = [NSKeyedArchiver archivedDataWithRootObject:newProfile requiringSecureCoding:YES error:nil];
|
||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles];
|
||||
[profilesEncoded addObject:newProfileEncoded];
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
OSCProfile* newProfile = nil;
|
||||
for(OSCProfile* profile in profiles){
|
||||
if(profile.isSelected){
|
||||
profile.isSelected = false;
|
||||
newProfile = [profile mutableCopy];
|
||||
newProfile.isSelected = true;
|
||||
newProfile.name = name;
|
||||
}
|
||||
}
|
||||
if(newProfile){
|
||||
NSData *newProfileEncoded = [NSKeyedArchiver archivedDataWithRootObject:newProfile requiringSecureCoding:YES error:nil];
|
||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles];
|
||||
[profilesEncoded addObject:newProfileEncoded];
|
||||
|
||||
/* Encode the 'profilesEncoded' array itself, NOT the objects in the 'profilesEncoded' array, all of which are already encoded by this point */
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
|
||||
// saving test:
|
||||
/*
|
||||
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
|
||||
NSString *path = [documentsPath stringByAppendingPathComponent:@"newDefault.bin"];
|
||||
|
||||
[profiles writeToFile:path atomically:YES];
|
||||
NSLog(@"默认 Profile 数据已保存到: %@", path);
|
||||
*/
|
||||
/* Encode the 'profilesEncoded' array itself, NOT the objects in the 'profilesEncoded' array, all of which are already encoded by this point */
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,8 +381,7 @@ static CGRect layoutViewBounds;
|
||||
position.x = position.x / layoutViewBounds.size.width;
|
||||
position.y = position.y / layoutViewBounds.size.height;
|
||||
}
|
||||
// asdfsda;
|
||||
NSLog(@"sef.view bounds: %f, %f", layoutViewBounds.size.width, layoutViewBounds.size.height);
|
||||
NSLog(@"layoutToolView bounds: %f, %f", layoutViewBounds.size.width, layoutViewBounds.size.height);
|
||||
NSLog(@"position: %f, %f, denormalized position: %f, %f", position.x, position.y, newPosition.x, newPosition.y);
|
||||
return position;
|
||||
}
|
||||
@@ -326,30 +394,32 @@ static CGRect layoutViewBounds;
|
||||
return position;
|
||||
}
|
||||
|
||||
- (NSMutableArray* ) convertOnScreenControllerAndWidgetsToButtonStates:(NSMutableArray *) oscButtonLayers {
|
||||
- (NSMutableArray* ) convertOnScreenControllerAndWidgetsToButtonStates:(NSMutableSet *) oscButtonLayers {
|
||||
/* iterate through each OSC button the user sees on screen, create an 'OnScreenButtonState' object from each button, encode each object, and then add each object to an array */
|
||||
/*
|
||||
NSSet *validPositionButtonNames = [NSSet setWithObjects:
|
||||
@"l2Button",
|
||||
@"l1Button",
|
||||
@"dPad",
|
||||
@"selectButton",
|
||||
@"leftStickBackground",
|
||||
@"rightStickBackground",
|
||||
@"r2Button",
|
||||
@"r1Button",
|
||||
@"aButton",
|
||||
@"bButton",
|
||||
@"xButton",
|
||||
@"yButton",
|
||||
@"startButton",
|
||||
nil]; */
|
||||
NSSet *validPositionButtonNames = [NSSet setWithObjects:
|
||||
@"l2Button",
|
||||
@"l1Button",
|
||||
@"dPad",
|
||||
@"selectButton",
|
||||
@"leftStickBackground",
|
||||
@"rightStickBackground",
|
||||
@"r2Button",
|
||||
@"r1Button",
|
||||
@"aButton",
|
||||
@"bButton",
|
||||
@"xButton",
|
||||
@"yButton",
|
||||
@"startButton",
|
||||
nil]; */
|
||||
NSMutableArray *buttonStatesEncoded = [[NSMutableArray alloc] init];
|
||||
|
||||
// save on-screen game controller buttons & sticks as buttonstate:
|
||||
for (CALayer *oscButtonLayer in oscButtonLayers) {
|
||||
if(oscButtonLayer.isHidden) continue;
|
||||
|
||||
CGPoint normalizedPosition = [self normalizeWidgetPosition:oscButtonLayer.position];
|
||||
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:oscButtonLayer.name buttonType:LegacyOscButton andPosition:normalizedPosition];
|
||||
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:oscButtonLayer.name buttonType:LegacyOnScreenControls andPosition:normalizedPosition];
|
||||
// add hidden attr here
|
||||
buttonState.isHidden = oscButtonLayer.isHidden;
|
||||
buttonState.oscLayerSizeFactor = [OnScreenControls getControllerLayerSizeFactor:oscButtonLayer];
|
||||
@@ -370,40 +440,71 @@ static CGRect layoutViewBounds;
|
||||
}
|
||||
|
||||
// save on-screen widget views (keyboard & mouse command) as buttonstate:
|
||||
_widgetSizeTransition = keepWidgetSize;
|
||||
for(OnScreenWidgetView* widgetView in OnScreenWidgetViews){
|
||||
CGPoint normalizedPosition = [self normalizeWidgetPosition:widgetView.center];
|
||||
CGPoint normalizedPosition = [self normalizeWidgetPosition:widgetView.storedCenter];
|
||||
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:widgetView.cmdString buttonType:CustomOnScreenWidget andPosition:normalizedPosition];
|
||||
buttonState.alias = widgetView.buttonLabel;
|
||||
buttonState.widthFactor = [self normalizeSizeWidthFactor:widgetView];
|
||||
NSLog(@"logging widthFactor %f", buttonState.widthFactor);
|
||||
buttonState.heightFactor = [self normalizeSizeHeightFactor:widgetView];
|
||||
buttonState.alias = widgetView.widgetLabel;
|
||||
buttonState.sequence = widgetView.sequence;
|
||||
buttonState.sequenceSet = widgetView.sequenceSet;
|
||||
buttonState.parentSequence = widgetView.parentSequence;
|
||||
buttonState.folded = widgetView.folded;
|
||||
buttonState.revealMode = widgetView.revealMode;
|
||||
buttonState.widthFactor = [self normalizeSizeWidthFactorWith:widgetView];
|
||||
buttonState.heightFactor = [self normalizeSizeHeightFactorWith:widgetView];
|
||||
buttonState.componentSizeFactor = [self normalizeComponentSizeFactorWith:widgetView];
|
||||
buttonState.backgroundAlpha = widgetView.backgroundAlpha;
|
||||
buttonState.labelAlpha = widgetView.labelAlpha;
|
||||
buttonState.borderAlpha = widgetView.borderAlpha;
|
||||
buttonState.highlightAlpha = widgetView.highlightAlpha;
|
||||
buttonState.borderWidth = widgetView.borderWidth;
|
||||
buttonState.highlightSizeFactor = widgetView.highlightSizeFactor;
|
||||
buttonState.autoTapInterval = widgetView.autoTapInterval;
|
||||
buttonState.vibrationStyle = widgetView.vibrationStyle;
|
||||
buttonState.mouseButtonAction = widgetView.mouseButtonAction;
|
||||
buttonState.sensitivityFactorX = widgetView.sensitivityFactorX;
|
||||
buttonState.sensitivityFactorY = widgetView.sensitivityFactorY;
|
||||
buttonState.decelerationRate = widgetView.trackballDecelerationRate;
|
||||
buttonState.slideThreshold = widgetView.slideThreshold;
|
||||
buttonState.yawFactor = widgetView.yawFactor;
|
||||
buttonState.pitchFactor = widgetView.pitchFactor;
|
||||
buttonState.rollFactor = widgetView.rollFactor;
|
||||
buttonState.decelerationRateX = widgetView.decelerationRateX;
|
||||
buttonState.decelerationRateY = widgetView.decelerationRateY;
|
||||
buttonState.stickIndicatorOffset = widgetView.stickIndicatorOffset;
|
||||
buttonState.widgetShape = widgetView.shape;
|
||||
buttonState.walkModeThreshold = widgetView.dWheelWalkModeThreshold;
|
||||
buttonState.minStickOffset = widgetView.minStickOffset;
|
||||
buttonState.slideMode = widgetView.slideMode;
|
||||
buttonState.buttonMode = widgetView.buttonMode;
|
||||
|
||||
NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
|
||||
[buttonStatesEncoded addObject: buttonStateEncoded];
|
||||
|
||||
}
|
||||
return buttonStatesEncoded;
|
||||
}
|
||||
|
||||
- (CGFloat)normalizeSizeWidthFactor:(OnScreenWidgetView* )widget{
|
||||
- (CGFloat)getReferenceLen{
|
||||
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
|
||||
return widget.bounds.size.width/screenWidthInPoints * 10000;
|
||||
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]);
|
||||
CGFloat longSideLen = fmax(screenWidthInPoints,screenHeightInPoints);
|
||||
CGFloat referenceLen = longSideLen;
|
||||
if(_widgetSizeTransition == keepWidgetSize) referenceLen = longSideLen;
|
||||
if(_widgetSizeTransition == transitionWithOrientation) referenceLen = screenWidthInPoints;
|
||||
return referenceLen;
|
||||
}
|
||||
|
||||
- (CGFloat)normalizeSizeHeightFactor:(OnScreenWidgetView* )widget{
|
||||
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
|
||||
return widget.bounds.size.height/screenWidthInPoints * 10000;
|
||||
- (CGFloat)normalizeSizeWidthFactorWith:(OnScreenWidgetView* )widget{
|
||||
return widget.bounds.size.width/[self getReferenceLen] * 10000;
|
||||
}
|
||||
|
||||
- (CGFloat)normalizeSizeHeightFactorWith:(OnScreenWidgetView* )widget{
|
||||
return widget.bounds.size.height/[self getReferenceLen] * 10000;
|
||||
}
|
||||
|
||||
- (CGFloat)normalizeComponentSizeFactorWith:(OnScreenWidgetView* )widget{
|
||||
if(widget.isStickWheel) {
|
||||
return widget.denormalizedComponentSizeFactor*widget.baselineDiameter/[self getReferenceLen] * 10000;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -432,7 +533,7 @@ static CGRect layoutViewBounds;
|
||||
|
||||
- (OnScreenButtonState *)unarchiveButtonStateEncoded:(NSData *)data {
|
||||
OnScreenButtonState* buttonState;
|
||||
buttonState = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObjects:[NSString class], [OnScreenButtonState class], nil]
|
||||
buttonState = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObjects:[NSString class], [OnScreenButtonState class], [NSSet class], [NSNumber class], nil]
|
||||
fromData:data
|
||||
error:nil];
|
||||
return buttonState;
|
||||
|
||||
@@ -24,46 +24,76 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@property NSString *name;
|
||||
@property NSString *alias;
|
||||
@property (nonatomic, assign) BOOL folded;
|
||||
@property (nonatomic, assign) uint8_t revealMode;
|
||||
@property (nonatomic, assign) int16_t sequence;
|
||||
@property (nonatomic, assign) int16_t parentSequence;
|
||||
@property NSSet *sequenceSet;
|
||||
@property CGPoint position;
|
||||
@property (nonatomic, assign) BOOL isHidden;
|
||||
@property (nonatomic, assign) uint8_t slideMode;
|
||||
@property (nonatomic, assign) uint8_t buttonType;
|
||||
@property (nonatomic, assign) uint8_t buttonMode;
|
||||
@property (nonatomic, assign) uint8_t widgetType;
|
||||
@property (nonatomic, assign) uint8_t sizeReference;
|
||||
@property (nonatomic, assign) CGFloat widthFactor; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat heightFactor; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat componentSizeFactor; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat borderWidth; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat highlightSizeFactor; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat sensitivityFactorX; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat sensitivityFactorY; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat decelerationRate; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat slideThreshold; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat yawFactor; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat pitchFactor; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat rollFactor; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat decelerationRateX; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat decelerationRateY; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat stickIndicatorOffset; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat walkModeThreshold; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat minStickOffset; // for OnScreenWidgetView
|
||||
@property NSString* widgetShape; // for OnScreenWidgetView
|
||||
|
||||
@property (nonatomic, assign) CGFloat oscLayerSizeFactor; // for OnScreenController CALayer
|
||||
@property (nonatomic, assign) CGFloat backgroundAlpha; // for OnScreenController CALayer
|
||||
@property (nonatomic, assign) uint8_t vibrationStyle; // for OnScreenController CALayer
|
||||
@property (nonatomic, assign) uint8_t mouseButtonAction; // for OnScreenController CALayer
|
||||
@property (nonatomic, assign) CGFloat backgroundAlpha;
|
||||
@property (nonatomic, assign) CGFloat labelAlpha;
|
||||
@property (nonatomic, assign) CGFloat borderAlpha;
|
||||
@property (nonatomic, assign) CGFloat highlightAlpha;
|
||||
@property (nonatomic, assign) uint8_t vibrationStyle;
|
||||
@property (nonatomic, assign) uint8_t mouseButtonAction;
|
||||
@property (nonatomic, assign) uint16_t autoTapInterval;
|
||||
|
||||
// @property (nonatomic, assign) BOOL hasValidPosition;
|
||||
|
||||
typedef NS_ENUM(NSInteger, OnScreenButtonType) {
|
||||
LegacyOscButton,
|
||||
typedef NS_ENUM(NSInteger, OnScreenWidgetType) {
|
||||
LegacyOnScreenControls,
|
||||
CustomOnScreenWidget
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, WidgetSizeReference) {
|
||||
longSide,
|
||||
shortSide
|
||||
};
|
||||
|
||||
|
||||
typedef NS_ENUM(NSInteger, MouseButtonAction) {
|
||||
hovering,
|
||||
leftButtonDown,
|
||||
middleButtonDown,
|
||||
rightButtonDown,
|
||||
noClick
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, ButtonSlideMode) {
|
||||
toggle,
|
||||
typedef NS_ENUM(NSInteger, ButtonMode) {
|
||||
slideToToggle,
|
||||
slideAndHold,
|
||||
disabled
|
||||
regular,
|
||||
tapToToggle,
|
||||
movable
|
||||
};
|
||||
|
||||
|
||||
typedef NS_ENUM(NSInteger, RevealMode) {
|
||||
coexist,
|
||||
exclusive
|
||||
};
|
||||
|
||||
- (id) initWithButtonName:(NSString*)name buttonType:(uint8_t)buttonType andPosition:(CGPoint)position;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
//
|
||||
|
||||
#import "OnScreenButtonState.h"
|
||||
#import "VoidLink-Swift.h"
|
||||
|
||||
@implementation OnScreenButtonState
|
||||
|
||||
@@ -19,7 +20,7 @@
|
||||
self.name = name;
|
||||
//self.isHidden = isHidden;
|
||||
self.position = position;
|
||||
self.buttonType = buttonType;
|
||||
self.widgetType = buttonType;
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -31,48 +32,82 @@
|
||||
|
||||
- (void) encodeWithCoder:(NSCoder*)encoder {
|
||||
[encoder encodeObject:self.name forKey:@"name"];
|
||||
[encoder encodeBool:self.folded forKey:@"folded"];
|
||||
[encoder encodeInt32:self.revealMode forKey:@"revealMode"];
|
||||
[encoder encodeInt32:self.sequence forKey:@"sequence"];
|
||||
[encoder encodeInt32:self.parentSequence forKey:@"parentSequence"];
|
||||
[encoder encodeObject:self.sequenceSet forKey:@"sequenceSet"];
|
||||
[encoder encodeObject:self.alias forKey:@"alias"];
|
||||
[encoder encodeInt:self.buttonType forKey:@"buttonType"];
|
||||
[encoder encodeInt:self.widgetType forKey:@"buttonType"]; // keep original key
|
||||
[encoder encodeInt:self.sizeReference forKey:@"sizeReference"];
|
||||
[encoder encodeInt:self.vibrationStyle forKey:@"vibrationStyle"];
|
||||
[encoder encodeInt:self.mouseButtonAction forKey:@"mouseButtonAction"];
|
||||
[encoder encodeInt:self.slideMode forKey:@"slideMode"];
|
||||
[encoder encodeInt:self.buttonMode forKey:@"slideMode"]; // buttonMode: previously slideMode, keep it for consistency
|
||||
[encoder encodeInt:self.autoTapInterval forKey:@"autoTapInterval"];
|
||||
[encoder encodeCGPoint:self.position forKey:@"position"];
|
||||
[encoder encodeBool:self.isHidden forKey:@"isHidden"];
|
||||
[encoder encodeFloat:self.widthFactor forKey:@"widthFactor"];
|
||||
[encoder encodeFloat:self.heightFactor forKey:@"heightFactor"];
|
||||
[encoder encodeFloat:self.componentSizeFactor forKey:@"componentSizeFactor"];
|
||||
[encoder encodeFloat:self.sensitivityFactorX forKey:@"sensitivityFactorX"];
|
||||
[encoder encodeFloat:self.sensitivityFactorY forKey:@"sensitivityFactorY"];
|
||||
[encoder encodeFloat:self.decelerationRate forKey:@"decelerationRate"];
|
||||
[encoder encodeFloat:self.slideThreshold forKey:@"slideThreshold"];
|
||||
[encoder encodeFloat:self.yawFactor forKey:@"yawFactor"];
|
||||
[encoder encodeFloat:self.pitchFactor forKey:@"pitchFactor"];
|
||||
[encoder encodeFloat:self.rollFactor forKey:@"rollFactor"];
|
||||
[encoder encodeFloat:self.decelerationRateX forKey:@"decelerationRateX"];
|
||||
[encoder encodeFloat:self.decelerationRateY forKey:@"decelerationRateY"];
|
||||
[encoder encodeFloat:self.stickIndicatorOffset forKey:@"stickIndicatorOffset"];
|
||||
[encoder encodeFloat:self.oscLayerSizeFactor forKey:@"oscLayerSizeFactor"];
|
||||
[encoder encodeFloat:self.backgroundAlpha forKey:@"backgroundAlpha"];
|
||||
[encoder encodeFloat:self.labelAlpha forKey:@"labelAlpha"];
|
||||
[encoder encodeFloat:self.borderAlpha forKey:@"borderAlpha"];
|
||||
[encoder encodeFloat:self.highlightAlpha forKey:@"highlightAlpha"];
|
||||
[encoder encodeFloat:self.borderWidth forKey:@"borderWidth"];
|
||||
[encoder encodeFloat:self.highlightSizeFactor forKey:@"highlightSizeFactor"];
|
||||
[encoder encodeObject:self.widgetShape forKey:@"widgetShape"];
|
||||
[encoder encodeFloat:self.walkModeThreshold forKey:@"walkModeThreshold"];
|
||||
[encoder encodeFloat:self.minStickOffset forKey:@"minStickOffset"];
|
||||
}
|
||||
|
||||
- (id) initWithCoder:(NSCoder*)decoder {
|
||||
if (self = [super init]) {
|
||||
self.name = [decoder decodeObjectForKey:@"name"];
|
||||
self.folded = [decoder containsValueForKey:@"folded"] ? [decoder decodeBoolForKey:@"folded"] : false;
|
||||
self.revealMode = [decoder containsValueForKey:@"revealMode"] ? [decoder decodeInt32ForKey:@"revealMode"] : coexist;
|
||||
self.sequence = [decoder containsValueForKey:@"sequence"] ? [decoder decodeInt32ForKey:@"sequence"] : -1;
|
||||
self.parentSequence = [decoder containsValueForKey:@"parentSequence"] ? [decoder decodeInt32ForKey:@"parentSequence"] : -1;
|
||||
self.sequenceSet = [decoder containsValueForKey:@"sequenceSet"] ? [decoder decodeObjectForKey:@"sequenceSet"] : [NSSet set];
|
||||
self.alias = [decoder decodeObjectForKey:@"alias"];
|
||||
self.buttonType = [decoder decodeIntForKey:@"buttonType"];
|
||||
self.vibrationStyle = [decoder decodeIntForKey:@"vibrationStyle"];
|
||||
self.widgetType = [decoder decodeIntForKey:@"buttonType"];
|
||||
self.sizeReference = [decoder containsValueForKey:@"sizeReference"] ? [decoder decodeIntForKey:@"sizeReference"] : longSide;
|
||||
self.vibrationStyle = [decoder containsValueForKey:@"vibrationStyle"] ? [decoder decodeIntForKey:@"vibrationStyle"] : UIImpactFeedbackStyleLight;
|
||||
self.mouseButtonAction = [decoder decodeIntForKey:@"mouseButtonAction"];
|
||||
self.slideMode = [decoder containsValueForKey:@"slideMode"] ? [decoder decodeIntForKey:@"slideMode"] : 0;
|
||||
self.buttonMode = [decoder containsValueForKey:@"slideMode"] ? [decoder decodeIntForKey:@"slideMode"] : 0;
|
||||
self.autoTapInterval = [decoder containsValueForKey:@"autoTapInterval"] ? [decoder decodeIntForKey:@"autoTapInterval"] : 45;
|
||||
self.position = [decoder decodeCGPointForKey:@"position"];
|
||||
self.isHidden = [decoder decodeBoolForKey:@"isHidden"];
|
||||
self.widthFactor = [decoder decodeFloatForKey:@"widthFactor"];
|
||||
self.heightFactor = [decoder decodeFloatForKey:@"heightFactor"];
|
||||
self.sensitivityFactorX = [decoder decodeFloatForKey:@"sensitivityFactorX"];
|
||||
self.sensitivityFactorX = self.sensitivityFactorX == 0 ? 1.0 : self.sensitivityFactorX;
|
||||
self.sensitivityFactorY = [decoder decodeFloatForKey:@"sensitivityFactorY"];
|
||||
self.sensitivityFactorY = self.sensitivityFactorY == 0 ? 1.0 : self.sensitivityFactorY;
|
||||
self.decelerationRate = [decoder decodeFloatForKey:@"decelerationRate"];
|
||||
self.componentSizeFactor = [decoder containsValueForKey:@"componentSizeFactor"] ? [decoder decodeFloatForKey:@"componentSizeFactor"] : 1.0;
|
||||
self.sensitivityFactorX = [decoder containsValueForKey:@"sensitivityFactorX"] ? [decoder decodeFloatForKey:@"sensitivityFactorX"] : 1.0;
|
||||
self.sensitivityFactorY = [decoder containsValueForKey:@"sensitivityFactorY"] ? [decoder decodeFloatForKey:@"sensitivityFactorY"] : 1.0;
|
||||
self.slideThreshold = [decoder containsValueForKey:@"slideThreshold"] ? [decoder decodeFloatForKey:@"slideThreshold"] : 6.0;
|
||||
self.yawFactor = [decoder containsValueForKey:@"yawFactor"] ? [decoder decodeFloatForKey:@"yawFactor"] : 1.0;
|
||||
self.pitchFactor = [decoder containsValueForKey:@"pitchFactor"] ? [decoder decodeFloatForKey:@"pitchFactor"] : 1.0;
|
||||
self.rollFactor = [decoder containsValueForKey:@"rollFactor"] ? [decoder decodeFloatForKey:@"rollFactor"] : 1.0;
|
||||
self.decelerationRateX = [decoder containsValueForKey:@"decelerationRateX"] ? [decoder decodeFloatForKey:@"decelerationRateX"] : 0.5;
|
||||
self.decelerationRateY = [decoder containsValueForKey:@"decelerationRateY"] ? [decoder decodeFloatForKey:@"decelerationRateY"] : 0.5;
|
||||
self.stickIndicatorOffset = [decoder decodeFloatForKey:@"stickIndicatorOffset"];
|
||||
self.oscLayerSizeFactor = [decoder decodeFloatForKey:@"oscLayerSizeFactor"];
|
||||
self.backgroundAlpha = [decoder decodeFloatForKey:@"backgroundAlpha"];
|
||||
self.backgroundAlpha = [decoder containsValueForKey:@"backgroundAlpha"] ? [decoder decodeFloatForKey:@"backgroundAlpha"] : 0.5;
|
||||
self.labelAlpha = [decoder containsValueForKey:@"labelAlpha"] ? [decoder decodeFloatForKey:@"labelAlpha"] : 0.82;
|
||||
self.borderAlpha = [decoder containsValueForKey:@"borderAlpha"] ? [decoder decodeFloatForKey:@"borderAlpha"] : 0.19;
|
||||
self.highlightAlpha = [decoder containsValueForKey:@"highlightAlpha"] ? [decoder decodeFloatForKey:@"highlightAlpha"] : 0.77;
|
||||
self.borderWidth = [decoder decodeFloatForKey:@"borderWidth"];
|
||||
self.highlightSizeFactor = [decoder containsValueForKey:@"highlightSizeFactor"] ? [decoder decodeFloatForKey:@"highlightSizeFactor"] : 1.0;
|
||||
self.widgetShape = [decoder decodeObjectForKey:@"widgetShape"];
|
||||
self.walkModeThreshold = [decoder containsValueForKey:@"walkModeThreshold"] ? [decoder decodeFloatForKey:@"walkModeThreshold"] : 16383;
|
||||
self.minStickOffset = [decoder decodeFloatForKey:@"minStickOffset"];
|
||||
}
|
||||
return self;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
CGFloat lowestTouchPointYCoord;
|
||||
}
|
||||
|
||||
|
||||
@property (nonatomic, weak) UIView* touchCapturingView;
|
||||
@property (nonatomic, assign) uint8_t numberOfTouchesRequired;
|
||||
@property (nonatomic, assign) bool immediateTriggering; // if enabled, trigger the signal on touchesBegan stage.
|
||||
@property (nonatomic, assign) double tapDownTimeThreshold; // tap down threshold in seconds.
|
||||
|
||||
@@ -39,13 +39,21 @@ static CGFloat screenWidthInPoints;
|
||||
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
// Check if the number of touches and taps meets the required criteria
|
||||
if ([[event allTouches] count] == _numberOfTouchesRequired) {
|
||||
_gestureCapturedTime = CACurrentMediaTime();
|
||||
_gestureCaptured = true;
|
||||
for(UITouch *touch in [event allTouches]){
|
||||
if(lowestTouchPointYCoord < [touch locationInView:self.view].y) lowestTouchPointYCoord = [touch locationInView:self.view].y;
|
||||
NSSet* allTouches = [event allTouches];
|
||||
if (allTouches.count == _numberOfTouchesRequired) {
|
||||
if([UITouchUtil touchesIn:_touchCapturingView from:event].count != allTouches.count){
|
||||
_gestureCaptured = false;
|
||||
self.state = UIGestureRecognizerStateFailed;
|
||||
return;
|
||||
}
|
||||
|
||||
_gestureCapturedTime = CACurrentMediaTime();
|
||||
_gestureCaptured = true;
|
||||
lowestTouchPointYCoord = 0;
|
||||
for(UITouch *touch in allTouches){
|
||||
if(lowestTouchPointYCoord < [touch locationInView:self.view].y) lowestTouchPointYCoord = [touch locationInView:self.view].y;
|
||||
// NSLog(@"lowest point coord: %f, %f", lowestTouchPointYCoord, CACurrentMediaTime());
|
||||
}
|
||||
// this mechanism is deprecated.
|
||||
/* if(_numberOfTouchesRequired == 2){
|
||||
NSArray *twoTouches = [[event allTouches] allObjects];
|
||||
@@ -55,13 +63,13 @@ static CGFloat screenWidthInPoints;
|
||||
_lowestTouchPointHeight = screenHeightInPoints - lowestTouchPointYCoord;
|
||||
// NSLog(@"gen _lowestTouchPointHeight %f markmark", _lowestTouchPointHeight);
|
||||
if(_immediateTriggering){
|
||||
lowestTouchPointYCoord = 0.0; //reset for next recoginition
|
||||
// lowestTouchPointYCoord = 0.0; //reset for next recoginition
|
||||
self.state = UIGestureRecognizerStateRecognized;
|
||||
return;
|
||||
}
|
||||
self.state = UIGestureRecognizerStatePossible;
|
||||
}
|
||||
if ([[event allTouches] count] > _numberOfTouchesRequired) {
|
||||
if (allTouches.count > _numberOfTouchesRequired) {
|
||||
_gestureCaptured = false;
|
||||
self.state = UIGestureRecognizerStateFailed;
|
||||
}
|
||||
@@ -76,40 +84,26 @@ static CGFloat screenWidthInPoints;
|
||||
_gestureCaptured = false;
|
||||
self.state = UIGestureRecognizerStateFailed;
|
||||
}
|
||||
else if(_gestureCaptured && allTouchesCount == [touches count] && !_isOnScreenControllerBeingPressed && ![self isOnScreenWidgetViewBeingPressed]){ //must exclude virtual controller & onscreen button taps here to prevent stucked button, _areVirtualControllerTaps flag is set by onscreencontrols, containOnScreenButtonsTaps will be returned by iterating all widget views in streamframeview
|
||||
else if(_gestureCaptured
|
||||
&& allTouchesCount == [touches count]
|
||||
&& !_isOnScreenControllerBeingPressed){ //must exclude virtual controller & onscreen button taps here to prevent stucked button, _areVirtualControllerTaps flag is set by onscreencontrols, containOnScreenButtonsTaps will be returned by iterating all widget views in streamframeview
|
||||
_gestureCaptured = false; //reset for next recognition
|
||||
if((CACurrentMediaTime() - _gestureCapturedTime) < _tapDownTimeThreshold){
|
||||
lowestTouchPointYCoord = 0.0; //reset for next recognition
|
||||
self.state = UIGestureRecognizerStateRecognized;
|
||||
// lowestTouchPointYCoord = 0.0; //reset for next recognition
|
||||
|
||||
for(UITouch* touch in touches){
|
||||
CGVector vector = [UITouchUtil vectorOf:touch in:self.view];
|
||||
if(hypot(vector.dx, vector.dy)>2){
|
||||
self.state = UIGestureRecognizerStateFailed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(self.state != UIGestureRecognizerStateFailed) self.state = UIGestureRecognizerStateRecognized;
|
||||
// NSLog(@"gen _lowestTouchPointHeight %f markmark touchesEnd", _lowestTouchPointHeight);
|
||||
}
|
||||
}
|
||||
if (allTouchesCount == [touches count]) _isOnScreenControllerBeingPressed = false; // need to reset this flag anyway, when all fingers are lefting
|
||||
}
|
||||
|
||||
|
||||
// it was a not a perfect choice to code OnScreenWidgetView in Swift...
|
||||
// we're unable to import this class to swift codebase by the bridging header,and have to exlucde the onscreen button taps here
|
||||
// by iterating every widget view instances. but it's basically ok since the number of widgetViews are always limited.
|
||||
// and this method will only be called when the recognizer is active & and the taps passes all the checks and is about to ge triggered.
|
||||
- (bool)isOnScreenWidgetViewBeingPressed {
|
||||
NSTimeInterval allFingersTapDownTime = CACurrentMediaTime() - _gestureCapturedTime; //RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S has been passed to this recognizer as _tapDownTimeShreshold, we'll decide how to deal with pressed flag of on-screen widget views based on tapDownTime
|
||||
bool gotOneWidgetPressed = false;
|
||||
// the tap gestures are all added to the streamFrameTopLayerView, where all the OnScreenWidgetViews are added. so we can iterate them in this way:
|
||||
for(UIView* view in self.view.subviews){
|
||||
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
|
||||
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
|
||||
if(gotOneWidgetPressed && allFingersTapDownTime <= _tapDownTimeThreshold){ // once we have just 1 button pressed already,& the tapDownTime has not exceeded the threshold, we'll reset pressed flag for the all the widgetViews.
|
||||
widgetView.pressed = false; //reset the flag for widgetView
|
||||
continue;
|
||||
}
|
||||
if(widgetView.pressed){
|
||||
gotOneWidgetPressed = true; //got one button pressed
|
||||
if(allFingersTapDownTime <= _tapDownTimeThreshold) widgetView.pressed = false; // reset the flag for current widgetView if tapDowntime is still within the threshold, if not, leave the flag for mouse scroller gesture in relative touch mode.
|
||||
}
|
||||
}
|
||||
}
|
||||
return gotOneWidgetPressed;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -5,8 +5,12 @@
|
||||
// Created by Cameron Gutman on 9/17/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 on 2025.10.12
|
||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HapticContext.h"
|
||||
#import "DataManager.h"
|
||||
|
||||
@import CoreHaptics;
|
||||
@import GameController;
|
||||
@@ -83,19 +87,74 @@
|
||||
}
|
||||
}
|
||||
|
||||
-(id) initWithGamepad:(GCController*)gamepad locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
||||
if (gamepad.haptics == nil) {
|
||||
Log(LOG_W, @"Controller %d does not support haptics", gamepad.playerIndex);
|
||||
-(id) initDeviceEngineContextWithGamepad:(GCController*)gamepad API_AVAILABLE(ios(13.0), tvos(13.0)) {
|
||||
NSError *error = nil;
|
||||
_hapticEngine = [[CHHapticEngine alloc] initAndReturnError:&error];
|
||||
if (error != nil) {
|
||||
Log(LOG_W, @"Controller %d: iPhone Haptic engine failed to start: %@", gamepad.playerIndex, error);
|
||||
return nil;
|
||||
}
|
||||
_playerIndex = gamepad.playerIndex;
|
||||
|
||||
[_hapticEngine startAndReturnError:&error];
|
||||
if (error != nil) {
|
||||
Log(LOG_W, @"Controller %d: Haptic engine failed to start: %@", gamepad.playerIndex, error);
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![[gamepad.haptics supportedLocalities] containsObject:locality]) {
|
||||
__weak typeof(self) weakSelf = self;
|
||||
_hapticEngine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) {
|
||||
HapticContext* me = weakSelf;
|
||||
if (me == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_W, @"Controller %d: Haptic engine stopped: %p", me->_playerIndex, stoppedReason);
|
||||
me->_hapticPlayer = nil;
|
||||
me->_hapticEngine = nil;
|
||||
me->_playing = NO;
|
||||
};
|
||||
_hapticEngine.resetHandler = ^{
|
||||
HapticContext* me = weakSelf;
|
||||
if (me == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_W, @"Controller %d: Haptic engine reset", me->_playerIndex);
|
||||
me->_hapticPlayer = nil;
|
||||
me->_playing = NO;
|
||||
[me->_hapticEngine startAndReturnError:nil];
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
-(id) initWithGamepad:(GCController*)gamepad locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
||||
bool fallBackToPhoneHaptics = false;
|
||||
|
||||
if (gamepad.haptics == nil) {
|
||||
Log(LOG_W, @"Controller %d does not support haptics", gamepad.playerIndex);
|
||||
fallBackToPhoneHaptics = true;
|
||||
}
|
||||
else if (![[gamepad.haptics supportedLocalities] containsObject:locality]) {
|
||||
Log(LOG_W, @"Controller %d does not support haptic locality: %@", gamepad.playerIndex, locality);
|
||||
return nil;
|
||||
fallBackToPhoneHaptics = true;
|
||||
}
|
||||
|
||||
if (fallBackToPhoneHaptics) {
|
||||
Log(LOG_W, @"Controller %d falls back to use iPhone Haptics for locality: %@", gamepad.playerIndex, locality);
|
||||
NSError *error = nil;
|
||||
_hapticEngine = [[CHHapticEngine alloc] initAndReturnError:&error];
|
||||
if (error != nil) {
|
||||
Log(LOG_W, @"Controller %d: iPhone Haptic engine failed to start: %@", gamepad.playerIndex, error);
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
else {
|
||||
_hapticEngine = [gamepad.haptics createEngineWithLocality:locality];
|
||||
}
|
||||
|
||||
_playerIndex = gamepad.playerIndex;
|
||||
_hapticEngine = [gamepad.haptics createEngineWithLocality:locality];
|
||||
|
||||
NSError* error;
|
||||
[_hapticEngine startAndReturnError:&error];
|
||||
@@ -136,7 +195,9 @@
|
||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightHandle];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
return [[HapticContext alloc] initDeviceEngineContextWithGamepad:gamepad];
|
||||
} else return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +206,9 @@
|
||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftHandle];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
return [[HapticContext alloc] initDeviceEngineContextWithGamepad:gamepad];
|
||||
} else return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +217,9 @@
|
||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftTrigger];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
return [[HapticContext alloc] initDeviceEngineContextWithGamepad:gamepad];
|
||||
} else return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +228,9 @@
|
||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightTrigger];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
return [[HapticContext alloc] initDeviceEngineContextWithGamepad:gamepad];
|
||||
} else return nil;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// InertialScroller.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025/12/5.
|
||||
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
class InertialScroller {
|
||||
public var decelerationRateX: CGFloat
|
||||
public var decelerationRateY: CGFloat
|
||||
|
||||
public lazy var timer: SafeTimer? = {
|
||||
SafeTimer(interval: 1/displayLinkRate) { [weak self] in
|
||||
guard let self = self, handler != nil else {return}
|
||||
self.vector.dx = self.vector.dx * decelerationRateX
|
||||
self.vector.dy = self.vector.dy * decelerationRateY
|
||||
if abs(self.vector.dx) < self.timerSuspendThreshold && abs(self.vector.dy) < self.timerSuspendThreshold {
|
||||
self.timer?.pause()
|
||||
}
|
||||
(self.handler ?? {})()
|
||||
}
|
||||
}()
|
||||
|
||||
public var vector: CGVector = .zero
|
||||
public var displayLinkRate: CGFloat
|
||||
public var timerSuspendThreshold: CGFloat = 0.06
|
||||
public var handler: (() -> Void)?
|
||||
|
||||
init(decelerationRate: CGFloat = 0.93, displayLinkRate: CGFloat = 60, handler: (() -> Void)? = nil) {
|
||||
self.decelerationRateX = decelerationRate
|
||||
self.decelerationRateY = decelerationRate
|
||||
self.displayLinkRate = displayLinkRate
|
||||
self.handler = handler
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,545 @@
|
||||
//
|
||||
// MicHandler.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025/9/2.
|
||||
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||
|
||||
import AVFoundation
|
||||
import Collections
|
||||
|
||||
@objc public protocol MicHandlerDelegate: AnyObject {
|
||||
@objc optional func micHandlerDidFinishPlayback(_ handler: MicHandler)
|
||||
@objc optional func micHandler(_ handler: MicHandler, didFailWithError error: NSError)
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
public class MicHandler: NSObject {
|
||||
|
||||
private var notificationTokens = [NSObjectProtocol]()
|
||||
private let engine = AVAudioEngine()
|
||||
private let playerNode = AVAudioPlayerNode()
|
||||
private var audioSink: Any?
|
||||
private var micInputFormat: AVAudioFormat!
|
||||
private var isRecording = false
|
||||
private var useBuiltinMic = false
|
||||
|
||||
private var pcm16BufferDeque = Deque<Int16>()
|
||||
private var pcm16BufferArray: [Int16] = []
|
||||
private let bufferQueue = DispatchQueue(label: "pcm.buffer.queue")
|
||||
private var timer: SafeTimer?
|
||||
private static var volume: Float = 1.0
|
||||
|
||||
/*
|
||||
private var recordedBuffers: [AVAudioPCMBuffer] = []
|
||||
private var recordedPCM16: [Data] = []
|
||||
private var recordedOpusPackets: [Data] = []
|
||||
*/
|
||||
|
||||
private var opusEncoder: OpaquePointer?
|
||||
private var opusDecoder: OpaquePointer?
|
||||
|
||||
private var sequenceNumber: UInt16 = 0
|
||||
private let ssrc: UInt32 = 0x12345678
|
||||
|
||||
private var globalTimestamp: TimeInterval = 0
|
||||
|
||||
|
||||
public weak var delegate: MicHandlerDelegate?
|
||||
|
||||
@objc public init(useBuiltinMic:Bool) {
|
||||
super.init()
|
||||
|
||||
let token = NotificationCenter.default.addObserver(
|
||||
forName: AVAudioSession.interruptionNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] notification in
|
||||
self?.handleInterruption(notification)
|
||||
}
|
||||
notificationTokens.append(token)
|
||||
|
||||
self.useBuiltinMic = useBuiltinMic
|
||||
do {
|
||||
try configureSession()
|
||||
try configureEngine()
|
||||
} catch {
|
||||
notify(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ----------- Mic permission -------------*/
|
||||
/// 请求麦克风权限
|
||||
/// - Parameter completion: 可选 block,如果为 nil 且未授权,会弹窗提示跳转系统设置
|
||||
@objc static func requestPermission(_ completion: ((Bool) -> Void)? = nil) {
|
||||
let permission = AVAudioSession.sharedInstance().recordPermission
|
||||
switch permission {
|
||||
case .granted:
|
||||
completion?(true)
|
||||
case .denied:
|
||||
if let callback = completion {
|
||||
callback(false)
|
||||
} else {
|
||||
showSettingsAlert()
|
||||
}
|
||||
case .undetermined:
|
||||
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
||||
DispatchQueue.main.async {
|
||||
if granted {
|
||||
completion?(true)
|
||||
} else {
|
||||
if let callback = completion {
|
||||
callback(false)
|
||||
} else {
|
||||
showSettingsAlert()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@unknown default:
|
||||
if let callback = completion {
|
||||
callback(false)
|
||||
} else {
|
||||
showSettingsAlert()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 弹窗提示用户跳转系统设置(英文版)
|
||||
private static func showSettingsAlert() {
|
||||
guard let topVC = topViewController() else { return }
|
||||
let alert = UIAlertController(
|
||||
title: SwiftLocalizationHelper.localizedString(forKey: "Microphone Permission") ,
|
||||
message: SwiftLocalizationHelper.localizedString(forKey: "micPermissionTip"),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Cancel"), style: .cancel, handler: nil))
|
||||
alert.addAction(UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Go to Settings") , style: .default, handler: { _ in
|
||||
openSettings()
|
||||
}))
|
||||
topVC.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
/// 打开系统设置
|
||||
@objc static func openSettings() {
|
||||
guard let url = URL(string: UIApplication.openSettingsURLString),
|
||||
UIApplication.shared.canOpenURL(url) else { return }
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
|
||||
/// 检查麦克风权限状态(返回 Int,OC 可用)
|
||||
@objc static func permissionGranted() -> Bool {
|
||||
return AVAudioSession.sharedInstance().recordPermission == AVAudioSession.RecordPermission.granted
|
||||
}
|
||||
|
||||
/// 获取最顶层 UIViewController
|
||||
private static func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
|
||||
if let nav = base as? UINavigationController {
|
||||
return topViewController(base: nav.visibleViewController)
|
||||
}
|
||||
if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
|
||||
return topViewController(base: selected)
|
||||
}
|
||||
if let presented = base?.presentedViewController {
|
||||
return topViewController(base: presented)
|
||||
}
|
||||
return base
|
||||
}
|
||||
/* ----------------------------------------*/
|
||||
|
||||
|
||||
/* ----------- Audio Session -------------*/
|
||||
|
||||
private func handleInterruption(_ notification: Notification) {
|
||||
guard let info = notification.userInfo,
|
||||
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
||||
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
|
||||
else { return }
|
||||
|
||||
switch type {
|
||||
case .began:
|
||||
self.stopTapping(stopEngine: true)
|
||||
case .ended:
|
||||
do {
|
||||
try configureEngine()
|
||||
} catch {
|
||||
notify(error)
|
||||
}
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
|
||||
self.startTapping()
|
||||
}
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func configureOpus(sampleRate: Int32, channels: Int) throws {
|
||||
var err: Int32 = 0
|
||||
guard let enc = opus_encoder_create(sampleRate, Int32(channels), OPUS_APPLICATION_VOIP, &err), err == OPUS_OK else {
|
||||
throw NSError(domain: "Opus", code: Int(err), userInfo: nil)
|
||||
}
|
||||
|
||||
opusEncoder = enc
|
||||
|
||||
guard let dec = opus_decoder_create(sampleRate, Int32(channels), &err), err == OPUS_OK else {
|
||||
throw NSError(domain: "Opus", code: Int(err), userInfo: nil)
|
||||
}
|
||||
opusDecoder = dec
|
||||
|
||||
// Optional: But defaults are fine. Only change when needed:
|
||||
opus_encoder_ctl_wrapper(enc, Int32(OPUS_SET_BITRATE_REQUEST), opus_int32(64000)) // Set bitrate
|
||||
opus_encoder_ctl_wrapper(enc, Int32(OPUS_SET_COMPLEXITY_REQUEST), opus_int32(5)) // Set complexity
|
||||
opus_encoder_ctl_wrapper(enc, Int32(OPUS_SET_SIGNAL_REQUEST), OPUS_SIGNAL_MUSIC) // Set signal type
|
||||
}
|
||||
|
||||
@objc public func startTapping() {
|
||||
// recordedBuffers.removeAll()
|
||||
isRecording = true
|
||||
self.timer?.start()
|
||||
/*
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.isRecording = false
|
||||
self.stop()
|
||||
self.playbackRecorded()
|
||||
self.playbackOpus()
|
||||
}*/
|
||||
}
|
||||
|
||||
@objc public func stopTapping(stopEngine:Bool) {
|
||||
isRecording = false
|
||||
self.timer?.pause()
|
||||
// engine.inputNode.removeTap(onBus: 0)
|
||||
if(stopEngine){
|
||||
playerNode.stop()
|
||||
engine.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private func configureSession() throws {
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
/*
|
||||
let bluetoothAudioOption = self.useBuiltinMic ? AVAudioSession.CategoryOptions.allowBluetoothA2DP : AVAudioSession.CategoryOptions.allowBluetooth
|
||||
try session.setCategory(.playAndRecord, mode: .default, options: [.mixWithOthers, .defaultToSpeaker, bluetoothAudioOption])
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
try session.setAllowHapticsAndSystemSoundsDuringRecording(true)
|
||||
} */
|
||||
// AVAudioSession Initiailized in Connection.m -> ArInit
|
||||
|
||||
// 列出所有可用输入
|
||||
if self.useBuiltinMic, let inputs = session.availableInputs {
|
||||
for port in inputs {
|
||||
if port.portType == .builtInMic {
|
||||
try session.setPreferredInput(port)
|
||||
print("Set preferred input to built-in mic")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// try session.setActive(true)
|
||||
}
|
||||
|
||||
private func sendOpusFrameFromDequeBuffer() {
|
||||
bufferQueue.sync {
|
||||
if pcm16BufferDeque.count >= 960 {
|
||||
let chunk = Array(pcm16BufferDeque.prefix(960))
|
||||
var packet = [UInt8](repeating: 0, count: 4000)
|
||||
guard let enc = self.opusEncoder else {return}
|
||||
let outBytes = opus_encode(enc, chunk, 960, &packet, Int32(packet.count))
|
||||
sendMicrophoneData(packet, outBytes)
|
||||
let removeCount = min(960, pcm16BufferDeque.count)
|
||||
if removeCount > 0 {
|
||||
pcm16BufferDeque.removeFirst(removeCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendOpusFrameFromArrayBuffer() {
|
||||
bufferQueue.sync {
|
||||
if pcm16BufferArray.count >= 960 {
|
||||
let chunk = Array(pcm16BufferArray.prefix(960))
|
||||
var packet = [UInt8](repeating: 0, count: 4000)
|
||||
guard let enc = self.opusEncoder else {return}
|
||||
let outBytes = opus_encode(enc, chunk, 960, &packet, Int32(packet.count))
|
||||
sendMicrophoneData(packet, outBytes)
|
||||
let removeCount = min(960, pcm16BufferArray.count)
|
||||
if removeCount > 0 {
|
||||
pcm16BufferArray.removeFirst(removeCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public static func setVolume(_ linearVolume: Float) {
|
||||
let clamped = max(0.0, min(1.5, linearVolume))
|
||||
let exponent: Float = 1.7
|
||||
MicHandler.volume = powf(clamped, exponent)
|
||||
}
|
||||
|
||||
private func configureEngine() throws {
|
||||
let input = engine.inputNode
|
||||
micInputFormat = input.inputFormat(forBus: 0)
|
||||
|
||||
try self.configureOpus(sampleRate: Int32(micInputFormat.sampleRate), channels: Int(micInputFormat.channelCount))
|
||||
|
||||
if #available(iOS 13.0, tvOS 13.0, *) {
|
||||
try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.02)
|
||||
|
||||
let sinkNode = AVAudioSinkNode { timestamp, frameCount, audioBufferList -> OSStatus in
|
||||
|
||||
guard self.isRecording else { return noErr}
|
||||
|
||||
let abl = audioBufferList.pointee.mBuffers
|
||||
guard let data = abl.mData?.assumingMemoryBound(to: Float.self) else { return noErr }
|
||||
let samples = UnsafeBufferPointer(start: data, count: Int(frameCount))
|
||||
|
||||
// 转 float32 -> int16
|
||||
var chunk = [Int16](repeating: 0, count: samples.count)
|
||||
for i in 0..<samples.count {
|
||||
let clamped = max(min(samples[i] * MicHandler.volume, 1.0), -1.0)
|
||||
chunk[i] = Int16(clamped * Float(Int16.max))
|
||||
}
|
||||
|
||||
// 追加到缓冲区
|
||||
self.bufferQueue.sync {
|
||||
self.pcm16BufferDeque.append(contentsOf: chunk)
|
||||
}
|
||||
|
||||
return noErr
|
||||
}
|
||||
|
||||
audioSink = sinkNode
|
||||
engine.attach(audioSink! as! AVAudioNode)
|
||||
engine.connect(engine.inputNode, to: audioSink as! AVAudioNode, format: micInputFormat)
|
||||
|
||||
// 开定时器,每 20ms 触发一次
|
||||
self.timer = SafeTimer(interval:0.02, delay: 0.05) {
|
||||
self.sendOpusFrameFromDequeBuffer()
|
||||
}
|
||||
}
|
||||
else{
|
||||
engine.attach(playerNode)
|
||||
engine.connect(playerNode, to: engine.mainMixerNode, format: micInputFormat)
|
||||
input.installTap(onBus: 0, bufferSize: 5760, format: micInputFormat) { [weak self] buffer, _ in
|
||||
guard let self = self, self.isRecording else { return }
|
||||
|
||||
// ===============================
|
||||
// 把 buffer 转成 PCM16 并保存
|
||||
let frameLength = Int(buffer.frameLength)
|
||||
let channels = Int(buffer.format.channelCount)
|
||||
var pcm16InterleavedBuffer = [Int16](repeating: 0, count: frameLength * channels)
|
||||
|
||||
if let floatPtrs = buffer.floatChannelData {
|
||||
for ch in 0..<channels {
|
||||
let floatPtr = floatPtrs[ch]
|
||||
for i in 0..<frameLength {
|
||||
let f = floatPtr[i] * MicHandler.volume
|
||||
// 把 float 转到 Int16 范围:假设 float 在 -1…+1 之间
|
||||
// 乘以 Int16.max (32767),再做裁剪
|
||||
let scaled = f * Float(Int16.max)
|
||||
let clipped: Float
|
||||
if scaled > Float(Int16.max) {
|
||||
clipped = Float(Int16.max)
|
||||
} else if scaled < Float(Int16.min) {
|
||||
clipped = Float(Int16.min)
|
||||
} else {
|
||||
clipped = scaled
|
||||
}
|
||||
pcm16InterleavedBuffer[i * channels + ch] = Int16(clipped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 现在 pcm16InterleavedBuffer 里就是转换后的 Int16 数据
|
||||
self.bufferQueue.sync {
|
||||
self.pcm16BufferArray.append(contentsOf: pcm16InterleavedBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
// 开定时器,每 20ms 触发一次
|
||||
self.timer = SafeTimer(interval:0.02, delay: 0.05) {
|
||||
self.sendOpusFrameFromArrayBuffer()
|
||||
}
|
||||
}
|
||||
|
||||
engine.prepare()
|
||||
try engine.start()
|
||||
}
|
||||
|
||||
@objc public func clean() {
|
||||
for token in notificationTokens {
|
||||
NotificationCenter.default.removeObserver(token)
|
||||
}
|
||||
notificationTokens.removeAll()
|
||||
self.timer?.clean()
|
||||
}
|
||||
|
||||
private func notify(_ error: Error) {
|
||||
delegate?.micHandler?(self, didFailWithError: error as NSError)
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 🔹 新增播放 Opus 数据方法
|
||||
/*
|
||||
private func playbackOpus() {
|
||||
guard let dec = opusDecoder else { return }
|
||||
|
||||
let channels = Int(micInputFormat.channelCount)
|
||||
|
||||
for (index, packet) in recordedOpusPackets.enumerated() {
|
||||
let maxFrames = 5760 // 最大 120ms
|
||||
let pcmBuf = UnsafeMutablePointer<Int16>.allocate(capacity: maxFrames * channels)
|
||||
defer { pcmBuf.deallocate() }
|
||||
|
||||
let frameCount = opus_decode(dec,
|
||||
[UInt8](packet),
|
||||
Int32(packet.count),
|
||||
pcmBuf,
|
||||
Int32(maxFrames),
|
||||
0)
|
||||
if frameCount < 0 {
|
||||
print("Opus decode error: \(frameCount)")
|
||||
continue
|
||||
}
|
||||
|
||||
// 构造源格式 AVAudioPCMBuffer (PCM16)
|
||||
let sourceFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
|
||||
sampleRate: micInputFormat.sampleRate,
|
||||
channels: micInputFormat.channelCount,
|
||||
interleaved: true)!
|
||||
|
||||
guard let sourceBuffer = AVAudioPCMBuffer(pcmFormat: sourceFormat,
|
||||
frameCapacity: AVAudioFrameCount(frameCount)) else { continue }
|
||||
sourceBuffer.frameLength = AVAudioFrameCount(frameCount)
|
||||
|
||||
// 填充 PCM16 数据到 sourceBuffer
|
||||
let srcPointer = sourceBuffer.int16ChannelData![0]
|
||||
for i in 0..<Int(frameCount * Int32(channels)) {
|
||||
srcPointer[i] = pcmBuf[i]
|
||||
}
|
||||
|
||||
// 准备目标 buffer (Float32)
|
||||
guard let floatBuffer = AVAudioPCMBuffer(pcmFormat: micInputFormat,
|
||||
frameCapacity: AVAudioFrameCount(frameCount)) else { continue }
|
||||
|
||||
// 🔹 使用 AVAudioConverter 转换
|
||||
let converter = AVAudioConverter(from: sourceFormat, to: micInputFormat)!
|
||||
var error: NSError? = nil
|
||||
let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
|
||||
outStatus.pointee = .haveData
|
||||
return sourceBuffer
|
||||
}
|
||||
|
||||
converter.convert(to: floatBuffer, error: &error, withInputFrom: inputBlock)
|
||||
if let error = error {
|
||||
print("AVAudioConverter error: \(error)")
|
||||
continue
|
||||
}
|
||||
|
||||
// 播放
|
||||
if index == recordedOpusPackets.count - 1 {
|
||||
playerNode.scheduleBuffer(floatBuffer, at: nil, options: []) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.playerNode.stop()
|
||||
self.engine.stop()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
playerNode.scheduleBuffer(floatBuffer, at: nil, options: [], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// 启动 engine
|
||||
if !engine.isRunning {
|
||||
do { try engine.start() } catch { print(error) }
|
||||
}
|
||||
|
||||
if !playerNode.isPlaying {
|
||||
playerNode.play()
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 🔹 改动 2:播放 PCM16 数据
|
||||
private func playbackRecorded() {
|
||||
do {
|
||||
try configureEngine()
|
||||
} catch {
|
||||
notify(error)
|
||||
}
|
||||
|
||||
let channels = Int(micInputFormat.channelCount)
|
||||
|
||||
for (index, pcmData) in recordedPCM16.enumerated() {
|
||||
let frameCount = pcmData.count / (MemoryLayout<Int16>.size * channels)
|
||||
guard let buf = AVAudioPCMBuffer(pcmFormat: micInputFormat, frameCapacity: AVAudioFrameCount(frameCount)) else { continue }
|
||||
buf.frameLength = buf.frameCapacity
|
||||
|
||||
// PCM16 -> Float32
|
||||
pcmData.withUnsafeBytes { rawBuf in
|
||||
let pcmPtr = rawBuf.bindMemory(to: Int16.self).baseAddress!
|
||||
for i in 0..<frameCount {
|
||||
for ch in 0..<channels {
|
||||
buf.floatChannelData?[ch][i] = Float(pcmPtr[i * channels + ch]) / Float(Int16.max)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 只在最后一个 buffer 设置 completionHandler
|
||||
if index == recordedPCM16.count - 1 {
|
||||
playerNode.scheduleBuffer(buf, at: nil, options: []) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
// 🔹 回到主线程安全停止
|
||||
DispatchQueue.main.async {
|
||||
self.playerNode.stop()
|
||||
self.engine.stop()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
playerNode.scheduleBuffer(buf, at: nil, options: [], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
playerNode.play()
|
||||
}
|
||||
|
||||
private func deepCopy(_ buffer: AVAudioPCMBuffer, format: AVAudioFormat) -> AVAudioPCMBuffer? {
|
||||
guard let out = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: buffer.frameLength) else { return nil }
|
||||
out.frameLength = buffer.frameLength
|
||||
let channels = Int(format.channelCount)
|
||||
for ch in 0..<channels {
|
||||
if let src = buffer.floatChannelData?[ch], let dst = out.floatChannelData?[ch] {
|
||||
dst.update(from: src, count: Int(buffer.frameLength))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private func merge(buffers: [AVAudioPCMBuffer], format: AVAudioFormat) -> AVAudioPCMBuffer? {
|
||||
let total = buffers.reduce(0) { $0 + $1.frameLength }
|
||||
guard let out = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: total) else { return nil }
|
||||
out.frameLength = total
|
||||
|
||||
var writePos: AVAudioFrameCount = 0
|
||||
let channels = Int(format.channelCount)
|
||||
|
||||
for b in buffers {
|
||||
let frames = Int(b.frameLength)
|
||||
for ch in 0..<channels {
|
||||
if let src = b.floatChannelData?[ch], let dst = out.floatChannelData?[ch] {
|
||||
dst.advanced(by: Int(writePos)).update(from: src, count: frames)
|
||||
}
|
||||
}
|
||||
writePos += b.frameLength
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------*/
|
||||
@@ -0,0 +1,385 @@
|
||||
//
|
||||
// MicHandler.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025/9/28.
|
||||
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreMotion
|
||||
|
||||
@objc public protocol OnScreenWidgetStickMixedInputDelegate: AnyObject {
|
||||
func mixOnScreenRightStickAndGyroInput(x: CGFloat, y: CGFloat)
|
||||
func mixOnScreenLeftStickAndGyroInput(x: CGFloat, y: CGFloat)
|
||||
func gyroMixInputStarted() -> Bool
|
||||
}
|
||||
|
||||
@objc class MotionHandler: NSObject, OnScreenWidgetStickMixedInputDelegate{
|
||||
public func mixOnScreenRightStickAndGyroInput(x: CGFloat, y: CGFloat) {
|
||||
rightStickTouchInputX = x
|
||||
rightStickTouchInputY = y
|
||||
}
|
||||
|
||||
public func mixOnScreenLeftStickAndGyroInput(x: CGFloat, y: CGFloat) {
|
||||
// rollIntegral = 0
|
||||
leftStickTouchInputX = x
|
||||
leftStickTouchInputY = y
|
||||
}
|
||||
|
||||
@objc public func mixPhysicalRightStickAndGyroInput(x: CGFloat, y: CGFloat) {
|
||||
rightStickPhysicalInputX = synthesizePhysicalStick ? x : 0
|
||||
rightStickPhysicalInputY = synthesizePhysicalStick ? y : 0
|
||||
}
|
||||
|
||||
@objc public func mixPhysicalLeftStickAndGyroInput(x: CGFloat, y: CGFloat) {
|
||||
// rollIntegral = 0
|
||||
leftStickPhysicalInputX = synthesizePhysicalStick ? x : 0
|
||||
leftStickPhysicalInputY = synthesizePhysicalStick ? y : 0
|
||||
}
|
||||
|
||||
public func gyroMixInputStarted() -> Bool {
|
||||
return gyroIsWorking
|
||||
}
|
||||
|
||||
private static let sharedInstance = MotionHandler()
|
||||
|
||||
@objc class func shared(profile: OSCProfile?) -> MotionHandler {
|
||||
let sharedInstance = MotionHandler.sharedInstance
|
||||
guard let profile = profile else { return sharedInstance }
|
||||
sharedInstance.mapGyroTo = profile.mapGyroTo
|
||||
sharedInstance.synthesizePhysicalStick = profile.synthesizePhysicalStick
|
||||
sharedInstance.rollToLeftStick = profile.rollToLeftStick
|
||||
sharedInstance.yawPitchToRightStick = profile.yawPitchToRightStick
|
||||
sharedInstance.sensitvityYaw = profile.gyroSensitivityYaw
|
||||
sharedInstance.sensitvityPitch = profile.gyroSensitivityPitch
|
||||
sharedInstance.sensitvityRoll = profile.gyroSensitivityRoll
|
||||
sharedInstance.gyroToStickMinOffset = profile.gyroToStickMinOffset
|
||||
sharedInstance.onScreenControls = OnScreenControls.shared()
|
||||
return sharedInstance
|
||||
}
|
||||
|
||||
@objc public var gyroControlStarted: Bool = false
|
||||
private var gyroIsWorking: Bool = false
|
||||
private var accelControlStarted: Bool = false
|
||||
private let motionManager = CMMotionManager()
|
||||
|
||||
private var synthesizePhysicalStick:Bool = false
|
||||
private var mapGyroTo:MapGyroTo = .mapGyroToControllerStick
|
||||
private var rollToLeftStick:Bool = false
|
||||
private var yawPitchToRightStick:Bool = false
|
||||
public var sensitvityYaw:CGFloat = 1.0
|
||||
public var sensitvityPitch:CGFloat = 1.0
|
||||
public var sensitvityRoll:CGFloat = 1.0
|
||||
@objc public var widgetYawFactor:CGFloat = 1.0
|
||||
@objc public var widgetPitchFactor:CGFloat = 1.0
|
||||
@objc public var widgetRollFactor:CGFloat = 1.0
|
||||
@objc public var previousWidgetYawFactor:CGFloat = 1.0
|
||||
@objc public var previousWidgetPitchFactor:CGFloat = 1.0
|
||||
@objc public var previousWidgetRollFactor:CGFloat = 1.0
|
||||
public var gyroStarter:Any?
|
||||
private var windowScene: Any?
|
||||
|
||||
@objc public var onScreenControls: OnScreenControls?
|
||||
public let stickMaxOffset: CGFloat = 0x7FFE
|
||||
private var stickInputScale: CGFloat = 35
|
||||
|
||||
private var yaw:Double = 0
|
||||
private var pitch:Double = 0
|
||||
private var roll:Double = 0
|
||||
private var rollIntegral:Double = 0
|
||||
|
||||
private var isCalibrating: Bool = false
|
||||
private var sumX: Double = 0
|
||||
private var sumY: Double = 0
|
||||
private var sumZ: Double = 0
|
||||
@objc public var gyroBiasX: Double = 0
|
||||
@objc public var gyroBiasY: Double = 0
|
||||
@objc public var gyroBiasZ: Double = 0
|
||||
@objc public var gyroToStickMinOffset:Double = 0
|
||||
private var yawBias:Double = 0
|
||||
private var pitchBias:Double = 0
|
||||
private var rollBias:Double = 0
|
||||
|
||||
private var rightStickTouchInputX:Double = 0
|
||||
private var rightStickTouchInputY:Double = 0
|
||||
|
||||
private var leftStickTouchInputX:Double = 0
|
||||
private var leftStickTouchInputY:Double = 0
|
||||
|
||||
private var rightStickPhysicalInputX:Double = 0
|
||||
private var rightStickPhysicalInputY:Double = 0
|
||||
public var gyroToStickOffset:CGVector = CGVector(dx: 0, dy: 0)
|
||||
|
||||
private var leftStickPhysicalInputX:Double = 0
|
||||
private var leftStickPhysicalInputY:Double = 0
|
||||
|
||||
var updateInterval: TimeInterval = 1.0 / 120.0 {
|
||||
didSet {
|
||||
motionManager.accelerometerUpdateInterval = updateInterval
|
||||
motionManager.gyroUpdateInterval = updateInterval
|
||||
}
|
||||
}
|
||||
|
||||
@objc public override init() {
|
||||
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||
super.init()
|
||||
if #available(iOS 13.0, *) {
|
||||
let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
|
||||
self.windowScene = scene
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
motionManager.accelerometerUpdateInterval = updateInterval
|
||||
motionManager.gyroUpdateInterval = updateInterval
|
||||
}
|
||||
|
||||
public func startGyroByOnScreenButton(_ sender: OnScreenWidgetView, yawFactor:CGFloat, pitchFactor:CGFloat, rollFactor:CGFloat){
|
||||
self.previousWidgetYawFactor = self.widgetYawFactor
|
||||
self.previousWidgetPitchFactor = self.widgetPitchFactor
|
||||
self.previousWidgetRollFactor = self.widgetRollFactor
|
||||
self.widgetYawFactor = yawFactor
|
||||
self.widgetPitchFactor = pitchFactor
|
||||
self.widgetRollFactor = rollFactor
|
||||
print("self.widgetRollFactor \(self.widgetRollFactor)")
|
||||
if self.gyroStarter == nil {
|
||||
self.gyroStarter = sender
|
||||
if sender.motionControlButtonString != "GYROPAUSE" {self.startGyroUpdate()}
|
||||
}
|
||||
else if sender.motionControlButtonString == "GYROPAUSE" {
|
||||
self.startGyroUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func startGyroByControllerButton(){
|
||||
self.previousWidgetYawFactor = self.widgetYawFactor
|
||||
self.previousWidgetPitchFactor = self.widgetPitchFactor
|
||||
self.previousWidgetRollFactor = self.widgetRollFactor
|
||||
self.widgetYawFactor = 1
|
||||
self.widgetPitchFactor = 1
|
||||
self.widgetRollFactor = 1
|
||||
self.startGyroUpdate()
|
||||
}
|
||||
|
||||
/// 启动传感器数据更新
|
||||
@objc public func startGyroUpdate() {
|
||||
if !gyroControlStarted {
|
||||
gyroControlStarted = true
|
||||
if motionManager.isGyroAvailable {
|
||||
motionManager.startGyroUpdates(to: .main) { [weak self] gyroData, _ in
|
||||
guard let self = self, let data = gyroData else { return }
|
||||
self.handleGyroData(x: data.rotationRate.x,
|
||||
y: data.rotationRate.y,
|
||||
z: data.rotationRate.z)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {return}
|
||||
}
|
||||
|
||||
@objc public func startAccelUpdate() {
|
||||
accelControlStarted = true
|
||||
if motionManager.isAccelerometerAvailable {
|
||||
motionManager.startAccelerometerUpdates(to: .main) { [weak self] accelData, _ in
|
||||
guard let self = self, let data = accelData else { return }
|
||||
self.handleAccelerometerData(x: data.acceleration.x,
|
||||
y: data.acceleration.y,
|
||||
z: data.acceleration.z)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止更新
|
||||
private var interruptNoneGyroInput:Bool = false
|
||||
|
||||
@objc public func stopGyroUpdate(interruptNoneGyroInput:Bool=false, resetLeftStick:Bool=false) {
|
||||
gyroControlStarted = false
|
||||
gyroIsWorking = false
|
||||
if motionManager.isGyroActive{
|
||||
motionManager.stopGyroUpdates()
|
||||
}
|
||||
self.clearGyroInput(interruptNonGyroInput:interruptNoneGyroInput, resetLeftStick:resetLeftStick)
|
||||
}
|
||||
|
||||
@objc public func stopAccelUpdate() {
|
||||
accelControlStarted = false
|
||||
if motionManager.isAccelerometerActive {
|
||||
motionManager.stopAccelerometerUpdates()
|
||||
}
|
||||
// self.onScreenControls.clearLeftStickTouchPadFlag()
|
||||
// self.onScreenControls.clearRightStickTouchPadFlag()
|
||||
}
|
||||
|
||||
// MARK: - 私有方法处理数据
|
||||
private func handleAccelerometerData(x: Double, y: Double, z: Double) {
|
||||
// 在这里处理加速度数据,比如计算方向、存储或驱动逻辑
|
||||
}
|
||||
|
||||
private func handleGyroData(x: Double, y: Double, z: Double) {
|
||||
|
||||
var yawSource:Double = 0
|
||||
var pitchSource:Double = 0
|
||||
var rollSource:Double = 0
|
||||
|
||||
/*
|
||||
let correctedX:Double = x - gyroBiasX
|
||||
let correctedY:Double = y - gyroBiasY
|
||||
let correctedZ:Double = z - gyroBiasZ */
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
let orientation = (windowScene as! UIWindowScene).interfaceOrientation
|
||||
switch orientation {
|
||||
case .landscapeLeft:
|
||||
yawBias = gyroBiasX
|
||||
pitchBias = -gyroBiasY
|
||||
rollBias = -gyroBiasZ
|
||||
yawSource = x-yawBias
|
||||
pitchSource = -y-pitchBias
|
||||
rollSource = -z-rollBias
|
||||
case .landscapeRight:
|
||||
yawBias = -gyroBiasX
|
||||
pitchBias = gyroBiasY
|
||||
rollBias = -gyroBiasZ
|
||||
yawSource = -x-yawBias
|
||||
pitchSource = y-pitchBias
|
||||
rollSource = -z-rollBias
|
||||
case .portrait:
|
||||
yawBias = -gyroBiasY
|
||||
pitchBias = -gyroBiasX
|
||||
rollBias = -gyroBiasZ
|
||||
yawSource = -y-yawBias
|
||||
pitchSource = -x-pitchBias
|
||||
rollSource = -z-rollBias
|
||||
case .portraitUpsideDown:
|
||||
yawBias = gyroBiasY
|
||||
pitchBias = gyroBiasX
|
||||
rollBias = -gyroBiasZ
|
||||
yawSource = y-yawBias
|
||||
pitchSource = x-pitchBias
|
||||
rollSource = -z-rollBias
|
||||
default:
|
||||
yawBias = gyroBiasX
|
||||
pitchBias = -gyroBiasY
|
||||
rollBias = -gyroBiasZ
|
||||
yawSource = x-yawBias
|
||||
pitchSource = -y-pitchBias
|
||||
rollSource = -z-rollBias
|
||||
}
|
||||
} else {
|
||||
let orientation = UIApplication.shared.statusBarOrientation
|
||||
if orientation.isLandscape {
|
||||
yawBias = gyroBiasX
|
||||
pitchBias = -gyroBiasY
|
||||
rollBias = -gyroBiasZ
|
||||
yawSource = x-yawBias
|
||||
pitchSource = -y-pitchBias
|
||||
rollSource = -z-rollBias
|
||||
} else if orientation.isPortrait {
|
||||
yawBias = -gyroBiasY
|
||||
pitchBias = -gyroBiasX
|
||||
rollBias = -gyroBiasZ
|
||||
yawSource = -y-yawBias
|
||||
pitchSource = -x-pitchBias
|
||||
rollSource = -z-rollBias
|
||||
}
|
||||
}
|
||||
|
||||
if !gyroControlStarted {
|
||||
self.clearGyroInput(interruptNonGyroInput: interruptNoneGyroInput)
|
||||
print("Gyro: stopped")
|
||||
return
|
||||
}
|
||||
|
||||
gyroIsWorking = true
|
||||
|
||||
if mapGyroTo == MapGyroTo.mapGyroToMouse {
|
||||
yaw = yawSource*sensitvityYaw*widgetYawFactor*30
|
||||
pitch = pitchSource*sensitvityPitch*widgetPitchFactor*30
|
||||
LiSendMouseMoveEvent(Int16(yaw),Int16(pitch))
|
||||
}
|
||||
|
||||
//guard let onScreenControls = onScreenControls else { return }
|
||||
|
||||
if mapGyroTo == MapGyroTo.mapGyroToControllerStick {
|
||||
if yawPitchToRightStick {
|
||||
gyroToStickOffset.dx = gyroInputToStickInput(input:yawSource*sensitvityYaw*widgetYawFactor*10)
|
||||
yaw = rightStickTouchInputX + rightStickPhysicalInputX + gyroToStickOffset.dx
|
||||
yaw = self.clampStickInput(input: yaw)
|
||||
|
||||
gyroToStickOffset.dy = -gyroInputToStickInput(input:pitchSource*sensitvityPitch*widgetPitchFactor*10)
|
||||
pitch = rightStickTouchInputY + rightStickPhysicalInputY + gyroToStickOffset.dy
|
||||
pitch = self.clampStickInput(input: pitch)
|
||||
|
||||
let offsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: yaw, dy: pitch), minOffset: gyroToStickMinOffset)
|
||||
|
||||
onScreenControls?.sendRightStickTouchPadEvent(offsetVector.dx, offsetVector.dy)
|
||||
}
|
||||
if rollToLeftStick {
|
||||
roll = gyroInputToStickInput(input:rollSource*sensitvityRoll*widgetRollFactor*0.2)
|
||||
rollIntegral = rollIntegral + roll
|
||||
|
||||
let mixedLeftStickOffsetX = self.clampStickInput(input: rollIntegral+leftStickTouchInputX+leftStickPhysicalInputX)
|
||||
let mixedLeftStickOffsetY = self.clampStickInput(input: leftStickTouchInputY+leftStickPhysicalInputY)
|
||||
|
||||
let offsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: mixedLeftStickOffsetX, dy: mixedLeftStickOffsetY), minOffset: gyroToStickMinOffset)
|
||||
|
||||
onScreenControls?.sendLeftStickTouchPadEvent(offsetVector.dx, offsetVector.dy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func clearGyroInput(interruptNonGyroInput:Bool, resetLeftStick:Bool=false){
|
||||
// guard let onScreenControls = onScreenControls else { return }
|
||||
|
||||
if yawPitchToRightStick{
|
||||
onScreenControls?.sendRightStickTouchPadEvent(rightStickPhysicalInputX+rightStickTouchInputX-yawBias, rightStickPhysicalInputY+rightStickTouchInputY-pitchBias)
|
||||
}
|
||||
if rollToLeftStick{
|
||||
if resetLeftStick {
|
||||
rollIntegral = 0
|
||||
onScreenControls?.sendLeftStickTouchPadEvent(leftStickPhysicalInputX+leftStickTouchInputX-rollBias,leftStickPhysicalInputY+leftStickTouchInputY)
|
||||
}
|
||||
}
|
||||
if(interruptNonGyroInput){
|
||||
onScreenControls?.clearLeftStickTouchPadFlag()
|
||||
onScreenControls?.clearRightStickTouchPadFlag()
|
||||
}
|
||||
}
|
||||
|
||||
private func clampStickInput(input: CGFloat) -> CGFloat{
|
||||
return fmax(fmin(input, stickMaxOffset),-stickMaxOffset)
|
||||
}
|
||||
|
||||
private func gyroInputToStickInput(input: CGFloat) -> CGFloat{
|
||||
return self.clampStickInput(input: stickMaxOffset * input / 8)
|
||||
}
|
||||
|
||||
@objc public func calibrateGyroBias(duration: TimeInterval = 5.0, completion: @escaping () -> Void) {
|
||||
guard motionManager.isGyroAvailable else { return }
|
||||
|
||||
isCalibrating = true
|
||||
var sumX = 0.0, sumY = 0.0, sumZ = 0.0
|
||||
var sampleCount = 0
|
||||
|
||||
motionManager.startGyroUpdates(to: .main) { [weak self] gyroData, _ in
|
||||
guard let self = self, self.isCalibrating, let data = gyroData else { return }
|
||||
sumX += data.rotationRate.x
|
||||
sumY += data.rotationRate.y
|
||||
sumZ += data.rotationRate.z
|
||||
sampleCount += 1
|
||||
}
|
||||
|
||||
// 5秒后计算平均值
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + duration) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.motionManager.stopGyroUpdates()
|
||||
if sampleCount > 0 {
|
||||
gyroBiasX = sumX / Double(sampleCount)
|
||||
gyroBiasY = sumY / Double(sampleCount)
|
||||
gyroBiasZ = sumZ / Double(sampleCount)
|
||||
}
|
||||
self.isCalibrating = false
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,11 @@
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NativeTouchHandler : UIResponder
|
||||
|
||||
@property (nonatomic, assign) NSMutableSet<UITouch *>* touchesCapturedByOnScreenButtons;
|
||||
|
||||
|
||||
- (id)initWithView:(StreamView* )view andSettings:(TemporarySettings*)settings;
|
||||
|
||||
- (void)setAllowSingleTouchEnabled:(BOOL)enabled;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -14,13 +14,11 @@
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
|
||||
@implementation NativeTouchHandler {
|
||||
StreamView* streamView;
|
||||
TemporarySettings* currentSettings;
|
||||
__weak StreamView* streamView;
|
||||
bool activateCoordSelector;
|
||||
bool singleTouchDisabled;
|
||||
CGFloat pointerVelocityDividerLocationByPoints;
|
||||
uint16_t touchMoveEventIntervalUs;
|
||||
|
||||
bool asyncNativeTouch;
|
||||
unsigned int touchDownQos;
|
||||
@@ -36,28 +34,36 @@
|
||||
NSMutableSet<NSNumber *> *unassignedPointerIds;
|
||||
NSMutableSet *blacklistedTouches;
|
||||
|
||||
bool trackPointEnabled;
|
||||
NSMutableArray<CAShapeLayer *> * trackPointPool; //pre-defined pool of pointerIds.
|
||||
|
||||
NSMutableDictionary *pointerObjDict;
|
||||
|
||||
CGFloat slideGestureVerticalThreshold;
|
||||
CGFloat screenWidthWithThreshold;
|
||||
CGFloat EDGE_TOLERANCE;
|
||||
CGFloat _edgeTolerance;
|
||||
|
||||
CGRect streamViewBounds;
|
||||
|
||||
int64_t moveEventIntervalNSec;
|
||||
}
|
||||
|
||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings{
|
||||
self = [super init];
|
||||
self->streamView = view;
|
||||
self->currentSettings = settings;
|
||||
self->activateCoordSelector = currentSettings.pointerVelocityModeDivider.floatValue != 1.0;
|
||||
self->touchMoveEventIntervalUs = currentSettings.touchMoveEventInterval.intValue;
|
||||
self->activateCoordSelector = settings.pointerVelocityModeDivider.floatValue != 1.0;
|
||||
self->moveEventIntervalNSec = (int64_t)(settings.touchMoveEventInterval.intValue * 1000);;
|
||||
self->streamViewBounds = view.bounds;
|
||||
|
||||
self->pointerIdDict = [NSMutableDictionary dictionary];
|
||||
self->pointerIdPool = [NSMutableSet set];
|
||||
self->trackPointEnabled = settings.touchPointTracking;
|
||||
self->trackPointPool = [NSMutableArray array];
|
||||
for (uint8_t i = 0; i <= 10; i++) { //ipadOS supports upto 11 finger touches
|
||||
[self->pointerIdPool addObject:@(i)];
|
||||
[self->trackPointPool addObject:[GraphicUtils makeTouchTrackpointIn:streamView]];
|
||||
}
|
||||
|
||||
self->activePointerIds = [NSMutableSet set];
|
||||
self->blacklistedTouches = [NSMutableSet set];
|
||||
|
||||
@@ -85,9 +91,9 @@
|
||||
|
||||
self->pointerObjDict = [NSMutableDictionary dictionary];
|
||||
|
||||
EDGE_TOLERANCE = 10.0;
|
||||
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
|
||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
|
||||
|
||||
self->pointerVelocityDividerLocationByPoints = self->streamView.bounds.size.width * settings.pointerVelocityModeDivider.floatValue;
|
||||
|
||||
@@ -164,7 +170,7 @@
|
||||
|
||||
//check if touch point is spawned on the left or right upper half screen edges, event to remote PC. this is for better handling in-stream slide gesture
|
||||
CGPoint initialPoint = [touch locationInView:self->streamView];
|
||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < _edgeTolerance || initialPoint.x > screenWidthWithThreshold)) {
|
||||
[blacklistedTouches addObject:touchAddrObj];
|
||||
}
|
||||
}
|
||||
@@ -187,10 +193,18 @@
|
||||
}
|
||||
|
||||
|
||||
- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType{
|
||||
- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType withEvent:(UIEvent *)event{
|
||||
//if(touchPointSpawnedAtUpperScreenEdge && touchType != LI_TOUCH_EVENT_UP) return; // we're done here. this touch event will not be sent to the remote PC. and this must be checked after coord selector finishes populating new relative coords, or the app will crash
|
||||
if([blacklistedTouches containsObject:@((uintptr_t)touch)]) return;
|
||||
|
||||
if(singleTouchDisabled
|
||||
&& [UITouchUtil touchesIn:streamView from:event].count != 2
|
||||
&& touchType != LI_TOUCH_EVENT_UP) return;
|
||||
|
||||
if(PencilHandler.pencilPausesNativeTouch
|
||||
&& PencilHandler.isDrawing
|
||||
&& touchType != LI_TOUCH_EVENT_UP) return;
|
||||
|
||||
CGPoint targetCoords;
|
||||
//NSLog(@"selecting coords: %d", touch.phase == UITouchPhaseMoved);
|
||||
// NSLog(@"excluded count: %d", (uint32_t)[excludedPointerIds count]);
|
||||
@@ -210,78 +224,87 @@
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchDownQos, 0), ^{
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
||||
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) continue;
|
||||
[self handleTouchDown:touch]; //generate & populate pointerId
|
||||
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN withEvent:event];
|
||||
}
|
||||
|
||||
if(self->trackPointEnabled){
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[CATransaction begin];
|
||||
[CATransaction setDisableActions:YES];
|
||||
for (UITouch* touch in touches){
|
||||
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
|
||||
trackPoint.position = [touch locationInView:self->streamView];
|
||||
trackPoint.hidden = false;
|
||||
}
|
||||
[CATransaction commit];
|
||||
});
|
||||
}
|
||||
});
|
||||
else{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
||||
[self handleTouchDown:touch]; //generate & populate pointerId
|
||||
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchMoveQos, 0), ^{
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, moveEventIntervalNSec), dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
||||
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) continue;
|
||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
||||
usleep(self->touchMoveEventIntervalUs);
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE withEvent:event];
|
||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords];
|
||||
}
|
||||
});
|
||||
else {
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
||||
// usleep(self->touchMoveEventIntervalUs);
|
||||
}
|
||||
|
||||
if(self->trackPointEnabled){
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[CATransaction begin];
|
||||
[CATransaction setDisableActions:YES];
|
||||
for (UITouch* touch in touches){
|
||||
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
|
||||
trackPoint.position = [touch locationInView:self->streamView];
|
||||
}
|
||||
[CATransaction commit];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if(asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchEndQos, 0), ^{
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, moveEventIntervalNSec), dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
||||
[self removePointerId:touch]; //then remove pointerId
|
||||
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]){
|
||||
[OnScreenControls.touchesCapturedByOnScreenControls removeObject:touch];
|
||||
continue;
|
||||
}
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP withEvent:event]; //send touch event before remove pointerId
|
||||
|
||||
if(self->trackPointEnabled){
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
|
||||
[CATransaction begin];
|
||||
[CATransaction setDisableActions:YES];
|
||||
trackPoint.hidden = true;
|
||||
[CATransaction commit];
|
||||
[self removePointerId:touch];
|
||||
});
|
||||
}
|
||||
else [self removePointerId:touch];
|
||||
|
||||
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
||||
[self->blacklistedTouches removeObject:@((uintptr_t)touch)];
|
||||
}
|
||||
//if(self->touchPointSpawnedAtUpperScreenEdge && [[event allTouches] count] == [touches count])
|
||||
});
|
||||
else{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
||||
[self removePointerId:touch]; //then remove pointerId
|
||||
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
[self touchesEnded:touches withEvent:event];
|
||||
}
|
||||
|
||||
|
||||
- (void)populatePointerObjIntoDict:(UITouch*)touch{
|
||||
NativeTouchPointer* pointer = [[NativeTouchPointer alloc] initWithTouch:touch];
|
||||
pointer.useRelativeCoords = pointer.initialPoint.x > pointerVelocityDividerLocationByPoints;
|
||||
@@ -310,4 +333,22 @@
|
||||
return pointer.useRelativeCoords ? pointer.latestRelativePoint : pointer.latestPoint;
|
||||
}
|
||||
|
||||
- (void)setAllowSingleTouchEnabled:(BOOL)enabled{
|
||||
singleTouchDisabled = !enabled;
|
||||
}
|
||||
|
||||
- (void)dealloc{
|
||||
NSLog(@"dealloc nativeTouchHanlder %f", CACurrentMediaTime());
|
||||
pointerIdDict = nil;
|
||||
activePointerIds = nil;
|
||||
pointerIdPool= nil;
|
||||
unassignedPointerIds = nil;
|
||||
blacklistedTouches = nil;
|
||||
for(CAShapeLayer* layer in trackPointPool){
|
||||
[layer removeFromSuperlayer];
|
||||
}
|
||||
trackPointPool = nil;
|
||||
pointerObjDict = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
||||
+ (void)initContextWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
||||
+ (void)cleanUpContext;
|
||||
|
||||
- (bool)doesNeedResetCoords;
|
||||
- (void)updatePointerCoords:(UITouch *)touch;
|
||||
|
||||
@@ -63,4 +63,8 @@ static StreamView* streamView;
|
||||
NSLog(@"stream wdith %f, stream height %f", streamViewWidth, streamViewHeight);
|
||||
}
|
||||
|
||||
+ (void)cleanUpContext {
|
||||
streamView = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -17,12 +17,7 @@
|
||||
|
||||
@class ControllerSupport;
|
||||
@class StreamConfiguration;
|
||||
|
||||
// sending self as an instance to OnScreenWidgetView or other related classes
|
||||
@protocol InstanceProviderDelegate <NSObject>
|
||||
- (void)getOnScreenControlsInstance:(id)sender; // Renamed delegate method
|
||||
@end
|
||||
|
||||
@class LayoutOnScreenControls;
|
||||
|
||||
static const float D_PAD_DIST = 10;
|
||||
static const float BUTTON_DIST = 20;
|
||||
@@ -36,13 +31,13 @@ static const float BUTTON_DIST = 20;
|
||||
CGFloat _dPadSizeFactor;
|
||||
}
|
||||
|
||||
+ (NSMutableDictionary *)layerVibrationStyleDic;
|
||||
+ (NSMutableDictionary *_Nullable)layerVibrationStyleDic;
|
||||
|
||||
typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
||||
OnScreenControlsLevelOff,
|
||||
OnScreenControlsLevelCustom,
|
||||
OnScreenControlsLevelSimple,
|
||||
OnScreenControlsLevelFull,
|
||||
OnScreenControlsLevelCustom,
|
||||
OnScreenControlsLevelAuto, // move it here instead of delete , to ensure integrity of the codes
|
||||
|
||||
// Internal levels selected by ControllerSupport
|
||||
@@ -52,11 +47,6 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@property (nonatomic, weak) id<InstanceProviderDelegate> delegate; // Delegate property
|
||||
- (void)sendInstance;
|
||||
|
||||
// @property (nonatomic, assign) CustomTapGestureRecognizer* mouseRightClickTapRecognizer; // this object will be passed to onscreencontrols class for areVirtualControllerTaps flag setting
|
||||
@property (nonatomic, assign) bool isLayingOut;
|
||||
//@property (nonatomic) NSMutableSet<UITouch* >* touchesCapturedByOnScreenButtons;
|
||||
@@ -105,12 +95,15 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
||||
|
||||
@property OnScreenControlsLevel _level;
|
||||
|
||||
@property NSMutableArray *OSCButtonLayers;
|
||||
@property NSMutableSet *OSCButtonLayerPool;
|
||||
@property NSMutableDictionary* layerVibrationStyleDic;
|
||||
|
||||
|
||||
|
||||
+ (NSMutableSet* )touchAddrsCapturedByOnScreenControls;
|
||||
+ (NSMutableSet* )touchesCapturedByOnScreenControls;
|
||||
+ (OnScreenControls* )shared;
|
||||
+ (void)setShared:(OnScreenControls*)instance;
|
||||
|
||||
- (void) sendRightStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
|
||||
- (void) clearRightStickTouchPadFlag;
|
||||
- (void) sendLeftStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
|
||||
@@ -125,18 +118,19 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
||||
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
|
||||
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
|
||||
- (void) setLevel:(OnScreenControlsLevel)level;
|
||||
- (void) show;
|
||||
- (void) showLegacyWidgetsWith:(OSCProfile* )profile;
|
||||
- (void) regenLayerPool;
|
||||
- (void) setupComplexControls;
|
||||
- (void) drawButtons;
|
||||
- (void) drawButtons:(OSCProfile* _Nullable)profile;
|
||||
- (void) drawBumpers;
|
||||
- (void) updateControls;
|
||||
- (void) updateLegacyWidgetsWith:(OSCProfile* _Nullable )profile;
|
||||
- (OnScreenControlsLevel) getLevel;
|
||||
- (void) setDPadCenter;
|
||||
- (void) setAnalogStickPositions;
|
||||
- (void) positionAndResizeSingleControllerLayers;
|
||||
- (void) resizeControllerLayerWith:(CALayer*)layer and:(CGFloat)sizeFactor;
|
||||
- (void) adjustControllerLayerOpacityWith:(CALayer*)layer and:(CGFloat)alpha;
|
||||
+ (CGFloat) getControllerLayerSizeFactor:(CALayer*)layer;
|
||||
- (CGFloat) getControllerLayerOpacity:(CALayer*)layer;
|
||||
- (void) setDPadCenter:(OSCProfile *_Nonnull)profile;;
|
||||
- (void) setAnalogStickPositions:(OSCProfile *_Nonnull)profile;
|
||||
- (void) positionAndResizeSingleControllerLayers:(OSCProfile *_Nonnull)profile;
|
||||
- (void) resizeControllerLayerWith:(CALayer*_Nonnull)layer and:(CGFloat)sizeFactor;
|
||||
- (void) adjustControllerLayerOpacityWith:(CALayer*_Nonnull)layer and:(CGFloat)alpha;
|
||||
+ (CGFloat) getControllerLayerSizeFactor:(CALayer*_Nonnull)layer;
|
||||
- (CGFloat) getControllerLayerOpacity:(CALayer*_Nonnull)layer;
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
//
|
||||
// PencilHandler.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025/11/17.
|
||||
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc class PencilHandler: UIResponder, UIPencilInteractionDelegate {
|
||||
@objc static var shared: PencilHandler?
|
||||
|
||||
/// 用来处理所有 touch 事件的宿主视图
|
||||
weak var streamView: UIView?
|
||||
// private var tickTimer: SafeTimer
|
||||
private var pencilInteraction: Any?
|
||||
private var streamAspectRatio: Float
|
||||
private var tickInterval: TimeInterval
|
||||
private var manualTick: Bool
|
||||
private var pencilTickEnabled: Bool
|
||||
private var pressureCurveEnabled: Bool = false
|
||||
private var manualHoverFlag: Bool = false
|
||||
@objc static var hoverSupported: Bool = false
|
||||
// @objc static private(set) var autoHoverTermination: Bool = false
|
||||
@objc static private(set) var hoverMode: PencilHoverMode = .HoverPencil
|
||||
private(set) var pencilProEnabled: Bool = false
|
||||
private var isFirstMove: Bool = false
|
||||
private var moveEventIndex: Int64 = 0
|
||||
private var initialMoveEventIndexLimit: Int64
|
||||
private var touchBeganForce: Float = 0
|
||||
@objc static private(set) var isDrawing: Bool = false
|
||||
@objc static private(set) var pencilPausesNativeTouch: Bool = false
|
||||
|
||||
// static private let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||
static private var selectedProfile:OSCProfile?
|
||||
|
||||
private var pressureLUT = PressureCurveLUT(curve: PressureCurve())
|
||||
|
||||
@objc init(streamView: UIView, settings: TemporarySettings) {
|
||||
self.streamView = streamView
|
||||
streamAspectRatio = settings.width.floatValue/settings.height.floatValue
|
||||
tickInterval = TimeInterval(settings.pencilTickIntervalUs.floatValue/1000000)
|
||||
manualTick = settings.pencilTickMode.intValue == PencilTickMode.ManualTick.rawValue
|
||||
pencilTickEnabled = settings.pencilTickMode.intValue != PencilTickMode.PencilTickDisabled.rawValue
|
||||
// initialMoveEventIndexLimit = UIScreen.main.maximumFramesPerSecond > 60 ? 4 : 2
|
||||
initialMoveEventIndexLimit = 0
|
||||
super.init()
|
||||
setupPressureLUT()
|
||||
PencilHandler.shared = self
|
||||
}
|
||||
|
||||
@objc public func setupPressureLUT(profile:OSCProfile? = nil){
|
||||
var selectedProfile:OSCProfile
|
||||
if profile == nil {
|
||||
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||
selectedProfile = oscProfileMan.getSelectedProfile()
|
||||
}
|
||||
else{
|
||||
selectedProfile = profile!
|
||||
}
|
||||
|
||||
doubleTapShorcuts.removeAll()
|
||||
if selectedProfile.doubleTapShorcutEnabled {
|
||||
if selectedProfile.eraserShortcut != "" {
|
||||
doubleTapShorcuts.append(selectedProfile.eraserShortcut)
|
||||
}
|
||||
if selectedProfile.brushShortcut != "" {
|
||||
doubleTapShorcuts.append(selectedProfile.brushShortcut)
|
||||
}
|
||||
}
|
||||
|
||||
PencilHandler.squeezeStartShortcut = ""
|
||||
PencilHandler.squeezeEndShortcut = ""
|
||||
if selectedProfile.squeezeShorcutEnabled {
|
||||
PencilHandler.squeezeStartShortcut = selectedProfile.squeezeStartShortcut
|
||||
PencilHandler.squeezeEndShortcut = selectedProfile.squeezeEndShortcut
|
||||
print("squeezeShorcutEnabled \(CACurrentMediaTime())")
|
||||
}
|
||||
|
||||
// PencilHandler.autoHoverTermination = selectedProfile.autoPencilHoverTermination
|
||||
PencilHandler.hoverMode = selectedProfile.pencilHoverMode
|
||||
|
||||
pressureCurveEnabled = selectedProfile.pressureCurveEnabled
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
IAPManager.checkPurchaseInfo(.PencilProPack) { info in
|
||||
self.pressureCurveEnabled = self.pressureCurveEnabled && info.valid
|
||||
self.pencilTickEnabled = self.pencilTickEnabled && info.valid
|
||||
self.pencilProEnabled = info.valid
|
||||
PencilHandler.pencilPausesNativeTouch = selectedProfile.pencilPausesNativeTouch && info.valid
|
||||
if info.valid {
|
||||
self.setupPencilInteraction(view: self.streamView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let persistedCurvePoints = PressureCurve.importCurvePoints(selectedProfile.pressureCurvePoints)
|
||||
let curve = PressureCurve()
|
||||
curve.polylinePoints = persistedCurvePoints
|
||||
curve.buildCurveSegments()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now()) {
|
||||
self.pressureLUT = PressureCurveLUT(curve: curve)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Touch Events
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard let event = event else { return }
|
||||
PencilHandler.isDrawing = true
|
||||
// isFirstMove = true
|
||||
moveEventIndex = 0
|
||||
for touch in touches {
|
||||
let coalesced = event.coalescedTouches(for: touch) ?? []
|
||||
_ = self.sendStylusEvent(touchBatch: pencilTickEnabled ? coalesced : [touch])
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard let event = event else { return }
|
||||
for touch in touches {
|
||||
let coalesced = event.coalescedTouches(for: touch) ?? []
|
||||
_ = self.sendStylusEvent(touchBatch: pencilTickEnabled ? coalesced : [touch])
|
||||
}
|
||||
moveEventIndex += 1
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard let event = event else { return }
|
||||
for touch in touches {
|
||||
let coalesced = event.coalescedTouches(for: touch) ?? []
|
||||
_ = self.sendStylusEvent(touchBatch: pencilTickEnabled ? coalesced : [touch])
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
self.touchesEnded(touches, with: event)
|
||||
}
|
||||
|
||||
// MARK: - Core Logic
|
||||
|
||||
func getVideoAreaSize()-> CGSize {
|
||||
guard let streamView = streamView else { return CGSize(width: 0, height: 0) }
|
||||
if streamView.bounds.size.width > streamView.bounds.size.height * CGFloat(streamAspectRatio) {
|
||||
return CGSizeMake(streamView.bounds.size.height * CGFloat(streamAspectRatio), streamView.bounds.size.height);
|
||||
} else {
|
||||
return CGSizeMake(streamView.bounds.size.width, streamView.bounds.size.width / CGFloat(streamAspectRatio));
|
||||
}
|
||||
}
|
||||
|
||||
func adjustCoordinatesForVideoArea(point: CGPoint) -> CGPoint {
|
||||
guard let streamView = streamView else { return CGPoint(x: 0, y: 0) }
|
||||
|
||||
var x = point.x - streamView.bounds.origin.x
|
||||
var y = point.y - streamView.bounds.origin.y
|
||||
|
||||
if x < streamView.bounds.width / 2 {
|
||||
x -= 1
|
||||
} else {
|
||||
x += 1
|
||||
}
|
||||
|
||||
if y < streamView.bounds.height / 2 {
|
||||
y -= 1
|
||||
} else {
|
||||
y += 1
|
||||
}
|
||||
|
||||
let videoSize = getVideoAreaSize() // 返回 CGSize
|
||||
let videoOrigin = CGPoint(
|
||||
x: streamView.bounds.width / 2 - videoSize.width / 2,
|
||||
y: streamView.bounds.height / 2 - videoSize.height / 2
|
||||
)
|
||||
|
||||
let confinedX = min(max(x, videoOrigin.x), videoOrigin.x + videoSize.width) - videoOrigin.x
|
||||
let confinedY = min(max(y, videoOrigin.y), videoOrigin.y + videoSize.height) - videoOrigin.y
|
||||
|
||||
return CGPoint(x: confinedX, y: confinedY)
|
||||
}
|
||||
|
||||
|
||||
func getRotation(fromAzimuthAngle azimuthAngle: Float) -> UInt16 {
|
||||
var rotationAngle = (azimuthAngle - .pi / 2) * (180.0 / .pi) // 弧度转角度
|
||||
if rotationAngle < 0 {
|
||||
rotationAngle += 360
|
||||
}
|
||||
return UInt16(rotationAngle)
|
||||
}
|
||||
|
||||
func getTilt(fromAltitudeAngle altitudeAngle: Float) -> UInt8 {
|
||||
let altitudeDegs = abs(Int16(altitudeAngle * (180.0 / .pi)))
|
||||
return UInt8(90 - min(90, Int(altitudeDegs)))
|
||||
}
|
||||
|
||||
private func sendStylusEvent(touchBatch: [UITouch], with event: UIEvent? = nil) -> DispatchTime {
|
||||
guard let streamView = streamView, touchBatch.count>0 else { return .now() }
|
||||
|
||||
var tickMoment: TimeInterval = 0
|
||||
var previousTimeStamp: TimeInterval = 0
|
||||
var delay:TimeInterval = 0
|
||||
var dispatchMoment: DispatchTime = .now()
|
||||
|
||||
for touch in touchBatch {
|
||||
let point = touch.preciseLocation(in: streamView)
|
||||
let azimuth = touch.azimuthAngle(in: streamView)
|
||||
let altitude = touch.altitudeAngle
|
||||
let location = self.adjustCoordinatesForVideoArea(point: point)
|
||||
let videoSize = self.getVideoAreaSize()
|
||||
let normalizedLocation = CGPoint(x: location.x/videoSize.width, y: location.y/videoSize.height)
|
||||
let force = Float(touch.force/touch.maximumPossibleForce)/sin(Float(altitude))
|
||||
var targetForce = pressureCurveEnabled ? self.pressureLUT.value(at: force) : force
|
||||
|
||||
let eventType:UInt8
|
||||
|
||||
switch touch.phase {
|
||||
case .began:
|
||||
eventType = UInt8(manualHoverFlag ? LI_TOUCH_EVENT_HOVER : LI_TOUCH_EVENT_DOWN)
|
||||
touchBeganForce = targetForce
|
||||
case .moved:
|
||||
eventType = UInt8(manualHoverFlag ? LI_TOUCH_EVENT_HOVER : LI_TOUCH_EVENT_MOVE)
|
||||
targetForce = moveEventIndex < initialMoveEventIndexLimit ? touchBeganForce : targetForce
|
||||
case .ended:
|
||||
eventType = UInt8(manualHoverFlag ? LI_TOUCH_EVENT_HOVER_LEAVE : LI_TOUCH_EVENT_UP)
|
||||
case .cancelled:
|
||||
eventType = UInt8(manualHoverFlag ? LI_TOUCH_EVENT_HOVER_LEAVE : LI_TOUCH_EVENT_UP)
|
||||
default:
|
||||
eventType = UInt8(LI_TOUCH_EVENT_HOVER)
|
||||
}
|
||||
|
||||
delay = manualTick ? 0.0086 : 0.0086
|
||||
delay = eventType == UInt8(LI_TOUCH_EVENT_UP) ? delay : 0
|
||||
|
||||
if previousTimeStamp == 0 {
|
||||
previousTimeStamp = touch.timestamp
|
||||
tickMoment = 0
|
||||
dispatchMoment = .now()
|
||||
}
|
||||
else {
|
||||
tickMoment += (manualTick ? tickInterval : touch.timestamp - previousTimeStamp)
|
||||
}
|
||||
|
||||
let sendableForce = targetForce
|
||||
DispatchQueue.global().asyncAfter(deadline: dispatchMoment + tickMoment + delay) {
|
||||
|
||||
/*
|
||||
if PencilHandler.autoHoverEnabled, eventType == UInt8(LI_TOUCH_EVENT_DOWN) {
|
||||
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER_LEAVE), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
|
||||
}
|
||||
*/
|
||||
|
||||
LiSendPenEvent(eventType, UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), sendableForce, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
|
||||
|
||||
if eventType == UInt8(LI_TOUCH_EVENT_UP) {
|
||||
PencilHandler.isDrawing = false
|
||||
if PencilHandler.hoverMode == .HoverDisabled || !PencilHandler.hoverSupported {
|
||||
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.0086){
|
||||
if !PencilHandler.isDrawing {
|
||||
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER_LEAVE), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dispatchMoment + tickMoment + delay
|
||||
}
|
||||
|
||||
@objc public func switchPencilHover(){
|
||||
manualHoverFlag = !manualHoverFlag
|
||||
}
|
||||
|
||||
@objc public func enablePencilHover(){
|
||||
manualHoverFlag = true
|
||||
}
|
||||
|
||||
@objc public func disablePencilHover(){
|
||||
manualHoverFlag = false
|
||||
}
|
||||
|
||||
@available(iOS 12.1, *)
|
||||
private func setupPencilInteraction(view:UIView?) {
|
||||
let interaction = UIPencilInteraction()
|
||||
guard let view else { return }
|
||||
if pencilInteraction != nil {return}
|
||||
interaction.delegate = self
|
||||
view.addInteraction(interaction)
|
||||
pencilInteraction = interaction
|
||||
}
|
||||
|
||||
@available(iOS 12.1, *)
|
||||
func pencilInteractionDidTap(_ interaction: UIPencilInteraction) {
|
||||
if !pencilProEnabled {return}
|
||||
if doubleTapShorcuts.isEmpty {return}
|
||||
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: doubleTapShorcuts[shortcutIndex])
|
||||
print("pencilInteractionDidTap \(CACurrentMediaTime())")
|
||||
CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: keyStrings, delay: 0.1)
|
||||
shortcutIndex = (shortcutIndex + 1) % doubleTapShorcuts.count
|
||||
}
|
||||
|
||||
@available(iOS 17.5, *)
|
||||
func pencilInteraction(
|
||||
_ interaction: UIPencilInteraction,
|
||||
didReceiveSqueeze squeeze: UIPencilInteraction.Squeeze
|
||||
) {
|
||||
if !pencilProEnabled {return}
|
||||
if PencilHandler.squeezeStartShortcut == "", PencilHandler.squeezeEndShortcut == "" {return}
|
||||
|
||||
let keepPressedUntilRelease = PencilHandler.squeezeEndShortcut.uppercased() == "NULL"
|
||||
let squeezePressKeyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: PencilHandler.squeezeStartShortcut)
|
||||
let squeezeReleaseKeyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: PencilHandler.squeezeEndShortcut)
|
||||
|
||||
guard let squeezePressKeyStrings = squeezePressKeyStrings else {return}
|
||||
guard squeezePressKeyStrings.count>0 else {return}
|
||||
|
||||
switch squeeze.phase {
|
||||
case .began:
|
||||
if keepPressedUntilRelease {CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: squeezePressKeyStrings, delay: 0.1, pressOnly: true)}
|
||||
else {CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: squeezePressKeyStrings, delay: 0.1)}
|
||||
break
|
||||
case .ended, .cancelled:
|
||||
if keepPressedUntilRelease {CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: squeezePressKeyStrings, releaseOnly: true)}
|
||||
else {CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: squeezeReleaseKeyStrings, delay: 0.1)}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc static private(set) var eraserShortcut:String = ""
|
||||
@objc static private(set) var brushShortcut:String = ""
|
||||
private var doubleTapShorcuts: Array<String> = []
|
||||
private var shortcutIndex: Int = 0
|
||||
|
||||
@objc func replaceBrush(with shortcut:String){
|
||||
if let i = doubleTapShorcuts.indices.last {
|
||||
doubleTapShorcuts[i] = shortcut
|
||||
}
|
||||
}
|
||||
|
||||
@objc func replaceEraser(with shortcut:String){
|
||||
if let i = doubleTapShorcuts.indices.first {
|
||||
doubleTapShorcuts[i] = shortcut
|
||||
}
|
||||
}
|
||||
|
||||
@objc static public func enterDoubleTapShortcuts(in viewController: UIViewController){
|
||||
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||
selectedProfile = oscProfileMan.getSelectedProfile()
|
||||
guard let selectedProfile = selectedProfile else {return}
|
||||
|
||||
let alert = UIAlertController(title: SwiftLocalizationHelper.localizedString(forKey: "Eraser Shortcut"),
|
||||
message: SwiftLocalizationHelper.localizedString(forKey: "Enter eraser keyboard shortcut:"),
|
||||
preferredStyle: .alert)
|
||||
|
||||
alert.addTextField { textField in
|
||||
textField.placeholder = SwiftLocalizationHelper.localizedString(forKey:"Example: e, ctrl+e, alt+e ...")
|
||||
textField.keyboardType = .asciiCapable
|
||||
textField.autocorrectionType = .no
|
||||
textField.spellCheckingType = .no
|
||||
textField.text = selectedProfile.eraserShortcut
|
||||
}
|
||||
|
||||
let okAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
|
||||
let comboButtons = alert.textFields?[0].text ?? ""
|
||||
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: comboButtons)
|
||||
if keyStrings?.count ?? 0 > 0 || comboButtons == "" {
|
||||
eraserShortcut = comboButtons
|
||||
}
|
||||
enterBrushShortcut(in: viewController)
|
||||
}
|
||||
|
||||
let learnMoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
|
||||
if let url = URL(string: SwiftLocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
alert.addAction(learnMoreAction)
|
||||
alert.addAction(okAction)
|
||||
|
||||
viewController.present(alert, animated: true, completion: {
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@objc static public func enterBrushShortcut(in viewController: UIViewController){
|
||||
|
||||
let alert = UIAlertController(title: SwiftLocalizationHelper.localizedString(forKey: "Brush Shortcut"),
|
||||
message: SwiftLocalizationHelper.localizedString(forKey: "Enter brush keyboard shortcut:"),
|
||||
preferredStyle: .alert)
|
||||
|
||||
alert.addTextField { textField in
|
||||
textField.placeholder = SwiftLocalizationHelper.localizedString(forKey:"Example: b, ctrl+b, alt+b ...")
|
||||
textField.keyboardType = .asciiCapable
|
||||
textField.autocorrectionType = .no
|
||||
textField.spellCheckingType = .no
|
||||
textField.text = selectedProfile?.brushShortcut
|
||||
}
|
||||
|
||||
let okAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
|
||||
let comboButtons = alert.textFields?[0].text ?? ""
|
||||
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: comboButtons)
|
||||
if keyStrings?.count ?? 0 > 0 || comboButtons == "" {
|
||||
brushShortcut = comboButtons
|
||||
}
|
||||
|
||||
if selectedProfile?.brushShortcut != brushShortcut
|
||||
|| selectedProfile?.eraserShortcut != eraserShortcut {
|
||||
guard let selectedProfile = selectedProfile else {return}
|
||||
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||
selectedProfile.brushShortcut = brushShortcut
|
||||
selectedProfile.eraserShortcut = eraserShortcut
|
||||
oscProfileMan.replaceSelectedProfile(with: selectedProfile, overwriteDefault: true)
|
||||
}
|
||||
}
|
||||
|
||||
let learnMoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
|
||||
if let url = URL(string: SwiftLocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
alert.addAction(learnMoreAction)
|
||||
alert.addAction(okAction)
|
||||
|
||||
viewController.present(alert, animated: true, completion: {
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@objc static private(set) var squeezeStartShortcut:String = ""
|
||||
@objc static private(set) var squeezeEndShortcut:String = ""
|
||||
|
||||
@objc static public func enterSqueezeShortcuts(in viewController: UIViewController){
|
||||
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||
selectedProfile = oscProfileMan.getSelectedProfile()
|
||||
guard let selectedProfile = selectedProfile else {return}
|
||||
|
||||
let alert = UIAlertController(title: SwiftLocalizationHelper.localizedString(forKey: "Squeeze Shortcut"),
|
||||
message: SwiftLocalizationHelper.localizedString(forKey: "enterSqueezePressShort"),
|
||||
preferredStyle: .alert)
|
||||
|
||||
alert.addTextField { textField in
|
||||
textField.placeholder = SwiftLocalizationHelper.localizedString(forKey:"Example: e, ctrl+e, alt+e ...")
|
||||
textField.keyboardType = .asciiCapable
|
||||
textField.autocorrectionType = .no
|
||||
textField.spellCheckingType = .no
|
||||
textField.text = selectedProfile.squeezeStartShortcut
|
||||
}
|
||||
|
||||
let okAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
|
||||
let comboButtons = alert.textFields?[0].text ?? ""
|
||||
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: comboButtons)
|
||||
if keyStrings?.count ?? 0 > 0 || comboButtons == "" {
|
||||
squeezeStartShortcut = comboButtons
|
||||
}
|
||||
enterSqueezeEndShortcut(in: viewController)
|
||||
}
|
||||
|
||||
let learnMoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
|
||||
if let url = URL(string: SwiftLocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
alert.addAction(learnMoreAction)
|
||||
alert.addAction(okAction)
|
||||
|
||||
viewController.present(alert, animated: true, completion: {
|
||||
})
|
||||
}
|
||||
|
||||
@objc static public func enterSqueezeEndShortcut(in viewController: UIViewController){
|
||||
|
||||
let alert = UIAlertController(title: SwiftLocalizationHelper.localizedString(forKey: "Squeeze Shortcut"),
|
||||
message: SwiftLocalizationHelper.localizedString(forKey: "enterSqueezeReleaseShort"),
|
||||
preferredStyle: .alert)
|
||||
|
||||
alert.addTextField { textField in
|
||||
textField.placeholder = SwiftLocalizationHelper.localizedString(forKey:"Example: b, ctrl+b, alt+b ...")
|
||||
textField.keyboardType = .asciiCapable
|
||||
textField.autocorrectionType = .no
|
||||
textField.spellCheckingType = .no
|
||||
textField.text = selectedProfile?.squeezeEndShortcut
|
||||
}
|
||||
|
||||
let okAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in
|
||||
let comboButtons = alert.textFields?[0].text ?? ""
|
||||
let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: comboButtons)
|
||||
if keyStrings?.count ?? 0 > 0 || comboButtons == "" {
|
||||
squeezeEndShortcut = comboButtons
|
||||
}
|
||||
|
||||
if selectedProfile?.squeezeEndShortcut != squeezeEndShortcut
|
||||
|| selectedProfile?.squeezeStartShortcut != squeezeStartShortcut {
|
||||
guard let selectedProfile = selectedProfile else {return}
|
||||
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||
selectedProfile.squeezeEndShortcut = squeezeEndShortcut
|
||||
selectedProfile.squeezeStartShortcut = squeezeStartShortcut
|
||||
oscProfileMan.replaceSelectedProfile(with: selectedProfile, overwriteDefault: true)
|
||||
}
|
||||
}
|
||||
|
||||
let learnMoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
|
||||
if let url = URL(string: SwiftLocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
alert.addAction(learnMoreAction)
|
||||
alert.addAction(okAction)
|
||||
|
||||
viewController.present(alert, animated: true, completion: {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
private func attachHoverLeave(normalizedLocation:CGPoint){
|
||||
DispatchQueue.global().asyncAfter(deadline: .now()+0.0086) {
|
||||
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0, 0)
|
||||
DispatchQueue.global().asyncAfter(deadline: .now()+0.0086) {
|
||||
// LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER_LEAVE), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func preTouchHoverActionAt(normalizedLocation:CGPoint){
|
||||
DispatchQueue.global().asyncAfter(deadline: .now()+0.0086) {
|
||||
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0, 0)
|
||||
DispatchQueue.global().asyncAfter(deadline: .now()+0.0086) {
|
||||
LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER_LEAVE), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,734 @@
|
||||
//
|
||||
// PressureCurve.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2026/1/5.
|
||||
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
// MARK: - PressureCurve
|
||||
final class PressureCurve {
|
||||
|
||||
var polylinePoints: [CGPoint] = [
|
||||
CGPoint(x: 0, y: 0.0),
|
||||
CGPoint(x: 1.0, y: 1.0)
|
||||
]
|
||||
|
||||
private var factor: CGFloat = 0.39
|
||||
|
||||
private var cachedSegments: [(p0: CGPoint, c1: CGPoint, c2: CGPoint, p1: CGPoint)] = []
|
||||
|
||||
private var curveCutPoints: [CGPoint] {
|
||||
guard polylinePoints.count >= 2 else { return polylinePoints }
|
||||
var cuts: [CGPoint] = []
|
||||
cuts.append(polylinePoints[0]) // 起点
|
||||
for i in 0..<(polylinePoints.count - 2) {
|
||||
let mid = CGPoint(
|
||||
x: (polylinePoints[i+1].x + polylinePoints[i+2].x)/2,
|
||||
y: (polylinePoints[i+1].y + polylinePoints[i+2].y)/2
|
||||
)
|
||||
if i < polylinePoints.count - 3 {
|
||||
cuts.append(mid)
|
||||
}
|
||||
}
|
||||
cuts.append(polylinePoints.last!) // 终点
|
||||
return cuts
|
||||
}
|
||||
|
||||
private func curveFactor(for middlePointCount: Int) -> CGFloat {
|
||||
guard middlePointCount > 0 else { return 2.0 / 3.0 }
|
||||
let n = CGFloat(middlePointCount)
|
||||
return 0.30538319 * pow(n, -2.1518917) + 0.36128348
|
||||
}
|
||||
|
||||
public func buildCurveSegments() {
|
||||
let cuts = curveCutPoints
|
||||
var segments: [(CGPoint, CGPoint, CGPoint, CGPoint)] = []
|
||||
|
||||
factor = curveFactor(for: polylinePoints.count-2)
|
||||
|
||||
for i in 0..<(cuts.count - 1) {
|
||||
let p0 = cuts[i]
|
||||
let p1 = cuts[i+1]
|
||||
|
||||
// segment index
|
||||
let startIndex = i
|
||||
let endIndex = i+1
|
||||
|
||||
let tangentStart: CGPoint
|
||||
if startIndex < polylinePoints.count - 1 {
|
||||
tangentStart = CGPoint(
|
||||
x: (polylinePoints[startIndex + 1].x - polylinePoints[startIndex].x) * factor,
|
||||
y: (polylinePoints[startIndex + 1].y - polylinePoints[startIndex].y) * factor
|
||||
)
|
||||
} else {
|
||||
tangentStart = .zero
|
||||
}
|
||||
|
||||
let tangentEnd: CGPoint
|
||||
if endIndex < polylinePoints.count - 1 {
|
||||
tangentEnd = CGPoint(
|
||||
x: (polylinePoints[endIndex + 1].x - polylinePoints[endIndex].x) * factor,
|
||||
y: (polylinePoints[endIndex + 1].y - polylinePoints[endIndex].y) * factor
|
||||
)
|
||||
} else {
|
||||
tangentEnd = .zero
|
||||
}
|
||||
|
||||
let c1 = CGPoint(x: p0.x + tangentStart.x, y: p0.y + tangentStart.y)
|
||||
let c2 = CGPoint(x: p1.x - tangentEnd.x, y: p1.y - tangentEnd.y)
|
||||
|
||||
segments.append((p0: p0, c1: c1, c2: c2, p1: p1))
|
||||
}
|
||||
|
||||
cachedSegments = segments
|
||||
}
|
||||
|
||||
/// 采样 曲线
|
||||
func sampleCurve(stepsPerSegment: Int = 50) -> [CGPoint] {
|
||||
var points: [CGPoint] = []
|
||||
buildCurveSegments()
|
||||
for seg in cachedSegments {
|
||||
let sampled = sampleCurve(p0: seg.p0, c1: seg.c1, c2: seg.c2, p1: seg.p1, steps: stepsPerSegment)
|
||||
if !points.isEmpty { points.removeLast() } // 避免重复点
|
||||
points.append(contentsOf: sampled)
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
private func sampleCurve(p0: CGPoint, c1: CGPoint, c2: CGPoint, p1: CGPoint, steps: Int) -> [CGPoint] {
|
||||
var result: [CGPoint] = []
|
||||
for i in 0...steps {
|
||||
let t = CGFloat(i)/CGFloat(steps)
|
||||
let mt = 1-t
|
||||
let x = mt*mt*mt*p0.x + 3*mt*mt*t*c1.x + 3*mt*t*t*c2.x + t*t*t*p1.x
|
||||
let y = mt*mt*mt*p0.y + 3*mt*mt*t*c1.y + 3*mt*t*t*c2.y + t*t*t*p1.y
|
||||
result.append(CGPoint(x:x, y:y))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var tangentPoints: [CGPoint] { polylinePoints }
|
||||
|
||||
func value(at x: CGFloat) -> CGFloat {
|
||||
|
||||
guard let firstPoint = polylinePoints.first else { return 0 }
|
||||
if x < firstPoint.x {return 0}
|
||||
|
||||
let xClamped = min(max(x, 0), 1)
|
||||
let segments = cachedSegments
|
||||
|
||||
for seg in segments {
|
||||
if xClamped >= seg.p0.x && xClamped <= seg.p1.x {
|
||||
let t = solveT(forX: xClamped,
|
||||
p0: seg.p0.x,
|
||||
c1: seg.c1.x,
|
||||
c2: seg.c2.x,
|
||||
p1: seg.p1.x)
|
||||
return cubic(p0: seg.p0.y,
|
||||
c1: seg.c1.y,
|
||||
c2: seg.c2.y,
|
||||
p1: seg.p1.y,
|
||||
t: t)
|
||||
}
|
||||
}
|
||||
|
||||
return segments.last?.p1.y ?? 0
|
||||
}
|
||||
|
||||
private func solveT(
|
||||
forX x: CGFloat,
|
||||
p0: CGFloat,
|
||||
c1: CGFloat,
|
||||
c2: CGFloat,
|
||||
p1: CGFloat,
|
||||
iterations: Int = 18
|
||||
) -> CGFloat {
|
||||
|
||||
var low: CGFloat = 0
|
||||
var high: CGFloat = 1
|
||||
var t: CGFloat = 0.5
|
||||
|
||||
for _ in 0..<iterations {
|
||||
t = (low + high) * 0.5
|
||||
let xt = cubic(p0: p0, c1: c1, c2: c2, p1: p1, t: t)
|
||||
|
||||
if xt < x {
|
||||
low = t
|
||||
} else {
|
||||
high = t
|
||||
}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
private func cubic(
|
||||
p0: CGFloat,
|
||||
c1: CGFloat,
|
||||
c2: CGFloat,
|
||||
p1: CGFloat,
|
||||
t: CGFloat
|
||||
) -> CGFloat {
|
||||
|
||||
let mt = 1 - t
|
||||
return mt*mt*mt*p0
|
||||
+ 3*mt*mt*t*c1
|
||||
+ 3*mt*t*t*c2
|
||||
+ t*t*t*p1
|
||||
}
|
||||
|
||||
static func exportCurvePoints(_ curve: PressureCurve) -> [NSNumber] {
|
||||
var arr: [NSNumber] = []
|
||||
for p in curve.polylinePoints {
|
||||
arr.append(NSNumber(value: Float(p.x)))
|
||||
arr.append(NSNumber(value: Float(p.y)))
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
static func importCurvePoints(_ numbers: [NSNumber]) -> [CGPoint] {
|
||||
var pts: [CGPoint] = []
|
||||
let count = numbers.count / 2
|
||||
|
||||
for i in 0..<count {
|
||||
let x = CGFloat(numbers[i*2].floatValue)
|
||||
let y = CGFloat(numbers[i*2 + 1].floatValue)
|
||||
pts.append(CGPoint(x: x, y: y))
|
||||
}
|
||||
return pts
|
||||
}
|
||||
|
||||
static func generateLinearCurvePoints(from pressures: [CGFloat]) -> [CGPoint] {
|
||||
guard !pressures.isEmpty else { return [] }
|
||||
|
||||
let samples = pressures.map { min(max($0, 0), 1) }
|
||||
let maxPressure = samples.max() ?? 0
|
||||
|
||||
if maxPressure < 1 {
|
||||
let platformX = maxPressure
|
||||
|
||||
return [
|
||||
CGPoint(x: 0, y: 0),
|
||||
CGPoint(x: platformX, y: 1),
|
||||
CGPoint(x: platformX, y: 1),
|
||||
CGPoint(x: 1, y: 1)
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
CGPoint(x: 0, y: 0),
|
||||
CGPoint(x: 1, y: 1)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - PressureCurveView
|
||||
final class PressureCurveView: UIView {
|
||||
|
||||
let curve = PressureCurve()
|
||||
|
||||
private var draggingIndex: Int? = nil
|
||||
private let hitRadius: CGFloat = 35
|
||||
|
||||
enum PressureTestStage: UInt8 {
|
||||
case drawingStage
|
||||
case curveStage
|
||||
}
|
||||
public var testStage: PressureTestStage = .curveStage
|
||||
private var pressures:[CGFloat] = []
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .black
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
public var showGraph: Bool = false {
|
||||
didSet { setNeedsDisplay() } // 改变开关立即刷新
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
guard let ctx = UIGraphicsGetCurrentContext() else { return }
|
||||
|
||||
ctx.saveGState()
|
||||
|
||||
// 缩放和平移
|
||||
let scale: CGFloat = 0.95
|
||||
let dx = (bounds.width - bounds.width * scale) / 2
|
||||
let dy = (bounds.height - bounds.height * scale) / 2
|
||||
ctx.translateBy(x: dx, y: dy)
|
||||
ctx.scaleBy(x: scale, y: scale)
|
||||
|
||||
if showGraph {
|
||||
drawGrid(ctx)
|
||||
drawCurve(ctx)
|
||||
drawTangents(ctx)
|
||||
}
|
||||
|
||||
ctx.restoreGState()
|
||||
}
|
||||
|
||||
private func drawGrid(_ ctx: CGContext) {
|
||||
ctx.setStrokeColor(UIColor.darkGray.cgColor)
|
||||
ctx.setLineWidth(1)
|
||||
for i in 0...4 {
|
||||
let t = CGFloat(i)/4
|
||||
let x = bounds.width*t
|
||||
let y = bounds.height*t
|
||||
ctx.move(to: CGPoint(x:x, y:0))
|
||||
ctx.addLine(to: CGPoint(x:x, y:bounds.height))
|
||||
ctx.move(to: CGPoint(x:0, y:y))
|
||||
ctx.addLine(to: CGPoint(x:bounds.width, y:y))
|
||||
}
|
||||
ctx.strokePath()
|
||||
}
|
||||
|
||||
private func drawCurve(_ ctx: CGContext) {
|
||||
ctx.setStrokeColor(UIColor.systemBlue.cgColor)
|
||||
ctx.setLineWidth(6)
|
||||
let points = curve.sampleCurve(stepsPerSegment: 60)
|
||||
for (i,p) in points.enumerated() {
|
||||
let mapped = map(p)
|
||||
if i==0 { ctx.move(to:mapped) } else { ctx.addLine(to:mapped) }
|
||||
}
|
||||
ctx.strokePath()
|
||||
}
|
||||
|
||||
private func drawTangents(_ ctx: CGContext) {
|
||||
let pts = curve.tangentPoints
|
||||
ctx.setStrokeColor(UIColor.systemPurple.cgColor)
|
||||
ctx.setLineWidth(2)
|
||||
for i in 0..<pts.count {
|
||||
let p = map(pts[i])
|
||||
if i==0 { ctx.move(to:p) } else { ctx.addLine(to:p) }
|
||||
}
|
||||
ctx.strokePath()
|
||||
|
||||
for p in pts {
|
||||
let c = map(p)
|
||||
let r: CGFloat = 16
|
||||
ctx.setFillColor(UIColor.systemPurple.cgColor)
|
||||
ctx.fill(CGRect(x:c.x-r/2, y:c.y-r/2, width:r, height:r))
|
||||
}
|
||||
}
|
||||
|
||||
private func map(_ p: CGPoint) -> CGPoint {
|
||||
CGPoint(x:p.x*bounds.width, y:(1-p.y)*bounds.height)
|
||||
}
|
||||
|
||||
private func unmap(_ p: CGPoint) -> CGPoint {
|
||||
CGPoint(
|
||||
x: p.x / bounds.width,
|
||||
y: 1 - (p.y / bounds.height)
|
||||
)
|
||||
}
|
||||
|
||||
private func hitTestPoint(_ location: CGPoint) -> Int? {
|
||||
let pts = curve.tangentPoints
|
||||
for (i, p) in pts.enumerated() {
|
||||
let c = map(p)
|
||||
let dx = c.x - location.x
|
||||
let dy = c.y - location.y
|
||||
if dx*dx + dy*dy <= hitRadius*hitRadius {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard let touch = touches.first else { return }
|
||||
let location = touch.location(in: self)
|
||||
|
||||
if testStage == .drawingStage {
|
||||
return
|
||||
}
|
||||
|
||||
draggingIndex = hitTestPoint(location)
|
||||
if draggingIndex == nil {
|
||||
guard
|
||||
let touch = touches.first
|
||||
else { return }
|
||||
let location = touch.location(in: self)
|
||||
var point = unmap(location)
|
||||
|
||||
point.x = min(max(point.x, 0), 1)
|
||||
point.y = min(max(point.y, 0), 1)
|
||||
var insertIdx:Int = -1
|
||||
|
||||
for (idx, polylinePoint) in curve.polylinePoints.enumerated() {
|
||||
if point.x <= polylinePoint.x {
|
||||
insertIdx = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
if insertIdx >= 0 {
|
||||
curve.polylinePoints.insert(point, at: insertIdx)
|
||||
draggingIndex = insertIdx
|
||||
}
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
if testStage == .drawingStage {
|
||||
let altitude = touches.first!.altitudeAngle
|
||||
let force = (touches.first!.force/touches.first!.maximumPossibleForce)/sin(altitude)
|
||||
pressures.append(force)
|
||||
return
|
||||
}
|
||||
|
||||
guard
|
||||
let touch = touches.first,
|
||||
let idx = draggingIndex
|
||||
else { return }
|
||||
|
||||
let location = touch.location(in: self)
|
||||
var point = unmap(location)
|
||||
|
||||
if draggingIndex == 0 {
|
||||
let nextPoint = curve.polylinePoints[idx+1]
|
||||
point.x = min(max(point.x, 0), nextPoint.x)
|
||||
// point.x = 0
|
||||
point.y = min(max(point.y, 0), 1)
|
||||
}
|
||||
else if draggingIndex == curve.polylinePoints.count-1 {
|
||||
point.x = 1
|
||||
point.y = min(max(point.y, 0), 1)
|
||||
}
|
||||
else {
|
||||
let nextPoint = curve.polylinePoints[idx+1]
|
||||
let previousPoint = curve.polylinePoints[idx-1]
|
||||
point.x = min(max(point.x, previousPoint.x), nextPoint.x)
|
||||
point.y = min(max(point.y, 0), 1)
|
||||
}
|
||||
|
||||
// print("currenPoint \(point)")
|
||||
|
||||
curve.polylinePoints[idx] = point
|
||||
setNeedsDisplay()
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
if testStage == .drawingStage {
|
||||
if pressures.isEmpty { return }
|
||||
curve.polylinePoints = PressureCurve.generateLinearCurvePoints(from: pressures)
|
||||
testStage = .curveStage
|
||||
showGraph = true
|
||||
let parentVC = parentViewController as! PressureCurveViewController
|
||||
parentVC.setupNavigationBar()
|
||||
// parentVC.showCurveStageTips()
|
||||
}
|
||||
draggingIndex = nil
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
draggingIndex = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class PressureCurveLUT {
|
||||
static let levels = 2048
|
||||
public let table: [Float]
|
||||
|
||||
init(curve: PressureCurve) {
|
||||
curve.buildCurveSegments()
|
||||
|
||||
var t = [Float](repeating: 0, count: Self.levels)
|
||||
|
||||
let maxIndex = Float(Self.levels - 1)
|
||||
|
||||
for i in 0..<Self.levels {
|
||||
let x = CGFloat(Float(i) / maxIndex)
|
||||
let y = curve.value(at: x)
|
||||
t[i] = Float(y)
|
||||
}
|
||||
|
||||
self.table = t
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
func value(at x: Float) -> Float {
|
||||
let clamped = min(max(x, 0), 1)
|
||||
let index = Int((clamped * Float(Self.levels - 1)).rounded())
|
||||
return table[index]
|
||||
}
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
var parentViewController: UIViewController? {
|
||||
var responder: UIResponder? = self
|
||||
while let r = responder {
|
||||
if let vc = r as? UIViewController {
|
||||
return vc
|
||||
}
|
||||
responder = r.next
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ViewController
|
||||
class PressureCurveViewController: UIViewController {
|
||||
|
||||
private var navBar = UINavigationBar()
|
||||
|
||||
private var container = UIView()
|
||||
private var curveView = PressureCurveView()
|
||||
|
||||
private var minPressure: CGFloat = 0
|
||||
private var maxPressure: CGFloat = 1
|
||||
private var isFirstLaunch: Bool = GenericUtils.isFirstLaunchPressureCurveTool()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.isOpaque = false
|
||||
self.view.backgroundColor = UIColor.clear // 半透明遮罩
|
||||
|
||||
let curveWidth: CGFloat = 530
|
||||
let curveHeight: CGFloat = 530
|
||||
curveView = PressureCurveView(frame: CGRect(x: 0, y: 0, width: curveWidth, height: curveHeight))
|
||||
curveView.center = self.view.center
|
||||
curveView.backgroundColor = .white
|
||||
curveView.layer.cornerRadius = 0
|
||||
curveView.layer.masksToBounds = true
|
||||
|
||||
container = UIView(frame: CGRect(x: 0, y: 0, width: curveWidth+10, height: curveHeight+50))
|
||||
container.center = CGPoint(x: self.view.bounds.midX, y: self.view.bounds.midY-20)
|
||||
container.backgroundColor = .white
|
||||
container.layer.cornerRadius = 12
|
||||
container.layer.masksToBounds = true
|
||||
container.layer.borderWidth = 2
|
||||
container.layer.borderColor = ThemeManager.separatorColor.cgColor
|
||||
container.addSubview(curveView)
|
||||
self.loadPersistedCurve()
|
||||
self.curveView.showGraph = true
|
||||
|
||||
self.setupNavigationBar()
|
||||
|
||||
self.view.addSubview(container)
|
||||
|
||||
curveView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
curveView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
|
||||
curveView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
|
||||
curveView.widthAnchor.constraint(equalToConstant: curveWidth),
|
||||
curveView.heightAnchor.constraint(equalToConstant: curveHeight)
|
||||
])
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(pencilProPurchaseAborted(_:)),
|
||||
name: AddOnProduct.PencilProPack.purchaseAbortedNotification(),
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(persistCurve),
|
||||
name: AddOnProduct.PencilProPack.purchaseSucceededNotification(),
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
/*
|
||||
AlertControllerUtil.cancelButtonString = SwiftLocalizationHelper.localizedString(forKey: "No")
|
||||
AlertControllerUtil.showAlert(
|
||||
in: self,
|
||||
title: SwiftLocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
|
||||
message: SwiftLocalizationHelper.localizedString(forKey:"Reset the pen pressure curve by starting from the pen pressure test?"),
|
||||
withCancel: true,
|
||||
buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "Yes"),
|
||||
countdown: 0,
|
||||
action: {},
|
||||
completion: {
|
||||
if AlertControllerUtil.actionCancelled {
|
||||
self.curveView.showGraph = true
|
||||
self.curveView.testStage = .curveStage
|
||||
self.curveView.setNeedsDisplay()
|
||||
self.setupNavigationBar()
|
||||
self.showCurveStageTips()
|
||||
}
|
||||
else {
|
||||
self.pressureRangeTest()
|
||||
}
|
||||
}
|
||||
) */
|
||||
if isFirstLaunch {
|
||||
showCurveStageTips()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
private func loadPersistedCurve() {
|
||||
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||
let persistedCurvePoints = PressureCurve.importCurvePoints(oscProfileMan.getSelectedProfile().pressureCurvePoints)
|
||||
curveView.curve.polylinePoints = persistedCurvePoints
|
||||
curveView.setNeedsDisplay()
|
||||
}
|
||||
|
||||
public func setupNavigationBar() {
|
||||
// 1. 创建 UINavigationBar
|
||||
navBar.removeFromSuperview()
|
||||
navBar = UINavigationBar()
|
||||
navBar.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.addSubview(navBar)
|
||||
|
||||
var navItem = UINavigationItem()
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
let appearance = UINavigationBarAppearance()
|
||||
appearance.configureWithTransparentBackground() // 透明背景
|
||||
appearance.shadowColor = nil // 去掉底部细线
|
||||
appearance.backgroundColor = .clear // 可以额外设置完全透明
|
||||
|
||||
navBar.standardAppearance = appearance
|
||||
navBar.scrollEdgeAppearance = appearance
|
||||
} else {
|
||||
navBar.setBackgroundImage(UIImage(), for: .default)
|
||||
navBar.shadowImage = UIImage() // 去掉底部线
|
||||
navBar.isTranslucent = true
|
||||
navBar.backgroundColor = .clear
|
||||
}
|
||||
|
||||
|
||||
// 2. 添加约束 (顶部,左右)
|
||||
NSLayoutConstraint.activate([
|
||||
navBar.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor),
|
||||
navBar.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||
navBar.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||
navBar.heightAnchor.constraint(equalToConstant: 42) // 导航栏标准高度
|
||||
])
|
||||
|
||||
|
||||
if curveView.testStage == .curveStage {
|
||||
let resetButton = UIBarButtonItem(title: SwiftLocalizationHelper.localizedString(forKey: "Reset"), style: .plain, target: self, action: #selector(resetTapped))
|
||||
let rangeTestButton = UIBarButtonItem(title: SwiftLocalizationHelper.localizedString(forKey: "Pressure-range-test"), style: .plain, target: self, action: #selector(pressureRangeTest))
|
||||
let saveButton = UIBarButtonItem(title: SwiftLocalizationHelper.localizedString(forKey: "Save"), style: .plain, target: self, action: #selector(saveTapped))
|
||||
let quitButton = UIBarButtonItem(title: SwiftLocalizationHelper.localizedString(forKey: "Exit"), style: .plain, target: self, action: #selector(exitTapped))
|
||||
navItem.leftBarButtonItems = [resetButton, rangeTestButton]
|
||||
navItem.rightBarButtonItems = [quitButton, saveButton]
|
||||
}
|
||||
|
||||
if curveView.testStage == .drawingStage {
|
||||
navItem = UINavigationItem(title: SwiftLocalizationHelper.localizedString(forKey: "Pressure Range Test"))
|
||||
}
|
||||
|
||||
// 4. 将 UINavigationItem 添加到导航栏
|
||||
navBar.items = [navItem]
|
||||
}
|
||||
|
||||
public func showCurveStageTips() {
|
||||
AlertControllerUtil.showAlert(
|
||||
in: self,
|
||||
title: SwiftLocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
|
||||
message: SwiftLocalizationHelper.localizedString(forKey:"Adjust the pressure curve by adding or dragging the purple squares."),
|
||||
withCancel: false,
|
||||
buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "This tip won't be shown again"),
|
||||
countdown: 6
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func resetTapped() {
|
||||
curveView.curve.polylinePoints = [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 1)]
|
||||
DispatchQueue.main.async {
|
||||
self.curveView.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func pencilProPurchaseAborted(_ notification: Notification) {
|
||||
guard let interruption = notification.object as? PurchaseInterruption else { return }
|
||||
if interruption != .learnMore {
|
||||
resetTapped()
|
||||
}
|
||||
if interruption == .lowOSVersion {
|
||||
AlertControllerUtil.showAlert(
|
||||
in: self,
|
||||
title: "",
|
||||
message: SwiftLocalizationHelper.localizedString(forKey:"PencilProPackLowOSVersionTip"),
|
||||
withCancel: false,
|
||||
buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "OK"),
|
||||
countdown: 0)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func pressureRangeTest() {
|
||||
AlertControllerUtil.showAlert(
|
||||
in: self,
|
||||
title: SwiftLocalizationHelper.localizedString(forKey: "Pressure Range Test"),
|
||||
message: SwiftLocalizationHelper.localizedString(forKey:"pressureRangeTestTip"),
|
||||
withCancel: true,
|
||||
buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "OK"),
|
||||
countdown: 0,
|
||||
completion:{
|
||||
if AlertControllerUtil.actionCancelled { return }
|
||||
self.resetTapped()
|
||||
self.curveView.showGraph = false
|
||||
self.curveView.testStage = .drawingStage
|
||||
self.curveView.setNeedsDisplay()
|
||||
self.setupNavigationBar()
|
||||
})
|
||||
}
|
||||
|
||||
@objc private func persistCurve() {
|
||||
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||
let selectedProfile = oscProfileMan.getSelectedProfile()
|
||||
selectedProfile.pressureCurvePoints = PressureCurve.exportCurvePoints(curveView.curve)
|
||||
oscProfileMan.replaceSelectedProfile(with: selectedProfile, overwriteDefault: true)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if self.isFirstLaunch {
|
||||
AlertControllerUtil.showAlert(
|
||||
in: self,
|
||||
title: SwiftLocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
|
||||
message: SwiftLocalizationHelper.localizedString(forKey:"firstPressureCurvePersistTip"),
|
||||
withCancel: false,
|
||||
buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "This tip won't be shown again"),
|
||||
countdown: 11,
|
||||
completion: {
|
||||
PencilHandler.shared?.setupPressureLUT()
|
||||
})
|
||||
self.isFirstLaunch = false
|
||||
}
|
||||
else{
|
||||
AlertControllerUtil.autoCompletion = true
|
||||
AlertControllerUtil.showAlert(
|
||||
in: self,
|
||||
title: SwiftLocalizationHelper.localizedString(forKey: ""),
|
||||
message: SwiftLocalizationHelper.localizedString(forKey:"Pressure curve saved"),
|
||||
withCancel: false,
|
||||
buttonTitle: "",
|
||||
countdown: 1,
|
||||
completion: {
|
||||
PencilHandler.shared?.setupPressureLUT()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func saveTapped() {
|
||||
IAPManager.checkPurchaseInfo(.PencilProPack) { info in
|
||||
if info.valid {
|
||||
self.persistCurve()
|
||||
}
|
||||
else{
|
||||
IAPManager.inAppPurchaseAction(viewController: self, product: .PencilProPack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func exitTapped() {
|
||||
self.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface PureNativeTouchHandler : UIResponder
|
||||
|
||||
|
||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
||||
|
||||
|
||||
|
||||
@@ -15,11 +15,9 @@
|
||||
#include <Limelight.h>
|
||||
|
||||
@implementation PureNativeTouchHandler {
|
||||
StreamView* streamView;
|
||||
TemporarySettings* currentSettings;
|
||||
__weak StreamView* streamView;
|
||||
bool activateCoordSelector;
|
||||
CGFloat pointerVelocityDividerLocationByPoints;
|
||||
uint16_t touchMoveEventIntervalUs;
|
||||
|
||||
bool asyncNativeTouch;
|
||||
unsigned int touchDownQos;
|
||||
@@ -35,28 +33,37 @@
|
||||
NSMutableSet<NSNumber *> *unassignedPointerIds;
|
||||
NSMutableSet *blacklistedTouches;
|
||||
|
||||
bool trackPointEnabled;
|
||||
NSMutableArray<CAShapeLayer *> * trackPointPool;
|
||||
|
||||
NSMutableDictionary *pointerObjDict;
|
||||
|
||||
CGFloat slideGestureVerticalThreshold;
|
||||
CGFloat screenWidthWithThreshold;
|
||||
CGFloat EDGE_TOLERANCE;
|
||||
CGFloat _edgeTolerance;
|
||||
|
||||
CGRect streamViewBounds;
|
||||
|
||||
int64_t moveEventIntervalNSec;
|
||||
}
|
||||
|
||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings{
|
||||
self = [super init];
|
||||
self->streamView = view;
|
||||
self->currentSettings = settings;
|
||||
self->activateCoordSelector = currentSettings.pointerVelocityModeDivider.floatValue != 1.0;
|
||||
self->touchMoveEventIntervalUs = currentSettings.touchMoveEventInterval.intValue;
|
||||
|
||||
self->activateCoordSelector = settings.pointerVelocityModeDivider.floatValue != 1.0;
|
||||
self->moveEventIntervalNSec = (int64_t)(settings.touchMoveEventInterval.intValue * 1000);;
|
||||
self->streamViewBounds = view.bounds;
|
||||
|
||||
self->pointerIdDict = [NSMutableDictionary dictionary];
|
||||
self->pointerIdPool = [NSMutableSet set];
|
||||
self->trackPointEnabled = settings.touchPointTracking;
|
||||
self->trackPointPool = [NSMutableArray array];
|
||||
for (uint8_t i = 0; i <= 10; i++) { //ipadOS supports upto 11 finger touches
|
||||
[self->pointerIdPool addObject:@(i)];
|
||||
[self->trackPointPool addObject:[GraphicUtils makeTouchTrackpointIn:streamView]];
|
||||
}
|
||||
|
||||
self->activePointerIds = [NSMutableSet set];
|
||||
self->blacklistedTouches = [NSMutableSet set];
|
||||
|
||||
@@ -81,12 +88,11 @@
|
||||
default: break;
|
||||
}
|
||||
|
||||
|
||||
self->pointerObjDict = [NSMutableDictionary dictionary];
|
||||
|
||||
EDGE_TOLERANCE = 10.0;
|
||||
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
|
||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
|
||||
|
||||
self->pointerVelocityDividerLocationByPoints = self->streamView.bounds.size.width * settings.pointerVelocityModeDivider.floatValue;
|
||||
|
||||
@@ -163,7 +169,7 @@
|
||||
|
||||
//check if touch point is spawned on the left or right upper half screen edges, event to remote PC. this is for better handling in-stream slide gesture
|
||||
CGPoint initialPoint = [touch locationInView:self->streamView];
|
||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < _edgeTolerance || initialPoint.x > screenWidthWithThreshold)) {
|
||||
[blacklistedTouches addObject:touchAddrObj];
|
||||
}
|
||||
}
|
||||
@@ -209,65 +215,75 @@
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchDownQos, 0), ^{
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
[self handleTouchDown:touch]; //generate & populate pointerId
|
||||
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
||||
}
|
||||
|
||||
if(self->trackPointEnabled){
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[CATransaction begin];
|
||||
[CATransaction setDisableActions:YES];
|
||||
for (UITouch* touch in touches){
|
||||
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
|
||||
trackPoint.position = [touch locationInView:self->streamView];
|
||||
trackPoint.hidden = false;
|
||||
}
|
||||
[CATransaction commit];
|
||||
});
|
||||
}
|
||||
});
|
||||
else{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
[self handleTouchDown:touch]; //generate & populate pointerId
|
||||
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchMoveQos, 0), ^{
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, moveEventIntervalNSec), dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
||||
usleep(self->touchMoveEventIntervalUs);
|
||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords];
|
||||
}
|
||||
});
|
||||
else {
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
||||
// usleep(self->touchMoveEventIntervalUs);
|
||||
}
|
||||
|
||||
if(self->trackPointEnabled){
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[CATransaction begin];
|
||||
[CATransaction setDisableActions:YES];
|
||||
for (UITouch* touch in touches){
|
||||
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
|
||||
trackPoint.position = [touch locationInView:self->streamView];
|
||||
}
|
||||
[CATransaction commit];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if(asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchEndQos, 0), ^{
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, moveEventIntervalNSec), dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
||||
[self removePointerId:touch]; //then remove pointerId
|
||||
|
||||
if(self->trackPointEnabled){
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
|
||||
[CATransaction begin];
|
||||
[CATransaction setDisableActions:YES];
|
||||
trackPoint.hidden = true;
|
||||
[CATransaction commit];
|
||||
[self removePointerId:touch];
|
||||
});
|
||||
}
|
||||
else [self removePointerId:touch];
|
||||
|
||||
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
||||
[self->blacklistedTouches removeObject:@((uintptr_t)touch)];
|
||||
}
|
||||
//if(self->touchPointSpawnedAtUpperScreenEdge && [[event allTouches] count] == [touches count])
|
||||
});
|
||||
else{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
||||
[self removePointerId:touch]; //then remove pointerId
|
||||
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
@@ -303,5 +319,19 @@
|
||||
return pointer.useRelativeCoords ? pointer.latestRelativePoint : pointer.latestPoint;
|
||||
}
|
||||
|
||||
- (void)dealloc{
|
||||
NSLog(@"dealloc purePativeTouchHanlder %f", CACurrentMediaTime());
|
||||
pointerIdDict = nil;
|
||||
activePointerIds = nil;
|
||||
pointerIdPool= nil;
|
||||
unassignedPointerIds = nil;
|
||||
blacklistedTouches = nil;
|
||||
for(CAShapeLayer* layer in trackPointPool){
|
||||
[layer removeFromSuperlayer];
|
||||
}
|
||||
trackPointPool = nil;
|
||||
pointerObjDict = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
#import "DataManager.h"
|
||||
#import "CustomTapGestureRecognizer.h"
|
||||
|
||||
#define RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S 0.15
|
||||
#define RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S 0.1
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RelativeTouchHandler : UIResponder
|
||||
@property (nonatomic, readonly) CustomTapGestureRecognizer* mouseRightClickTapRecognizer; // this object will be passed to onscreencontrol class for areVirtualControllerTaps flag setting
|
||||
|
||||
@property (nonatomic, readonly) CustomTapGestureRecognizer* mouseRightClickTapRecognizer; // this object will be passed to onscreencontrol class for areVirtualControllerTaps flag setting
|
||||
|
||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// RelativeTouchHandler.m
|
||||
// Moonlight
|
||||
// VoidLink
|
||||
//
|
||||
// Completely refactored by True砖家 on 2024.9.13
|
||||
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
||||
@@ -23,22 +23,26 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
||||
BOOL firstTouchMoved;
|
||||
BOOL mousePointerMoved;
|
||||
BOOL quickTapDetected;
|
||||
BOOL isInMouseWheelScrollingMode;
|
||||
|
||||
// upper screen edge check
|
||||
bool touchPointSpawnedAtUpperScreenEdge;
|
||||
CGFloat slideGestureVerticalThreshold;
|
||||
CGFloat screenWidthWithThreshold;
|
||||
CGFloat EDGE_TOLERANCE;
|
||||
CGFloat _edgeTolerance;
|
||||
|
||||
UITouch* touchLockedForMouseMove;
|
||||
UITouch* quickTapTouch;
|
||||
|
||||
bool multiTouchesDetected;
|
||||
|
||||
CADisplayLink *displayLink;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
UIGestureRecognizer* remotePressRecognizer;
|
||||
UIGestureRecognizer* remoteLongPressRecognizer;
|
||||
#endif
|
||||
|
||||
StreamView* streamView;
|
||||
__weak StreamView* streamView;
|
||||
}
|
||||
|
||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings {
|
||||
@@ -53,18 +57,21 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
||||
_mouseRightClickTapRecognizer.delaysTouchesBegan = NO;
|
||||
_mouseRightClickTapRecognizer.delaysTouchesEnded = NO;
|
||||
[self->streamView.streamFrameTopLayerView addGestureRecognizer:_mouseRightClickTapRecognizer]; // add all additional gestures to the streamFrameTopLayerView instead of the streamview.
|
||||
_mouseRightClickTapRecognizer.touchCapturingView = streamView;
|
||||
|
||||
isInMouseWheelScrollingMode = false;
|
||||
firstTouchMoved = false;
|
||||
mousePointerMoved = false;
|
||||
mousePointerTimestamp = 0;
|
||||
|
||||
// upper screen check
|
||||
EDGE_TOLERANCE = 10.0;
|
||||
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
|
||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
|
||||
self->touchPointSpawnedAtUpperScreenEdge = false;
|
||||
|
||||
// self->displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
|
||||
// [self->displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)];
|
||||
remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
|
||||
@@ -81,42 +88,21 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
||||
|
||||
- (bool)isOnScreenControllerBeingPressed:(NSSet* )touches{
|
||||
for(UITouch* touch in touches){
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) return true;
|
||||
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
- (bool)isOnScreenWidgetViewBeingPressed {
|
||||
bool gotOneButtonPressed = false;
|
||||
for(UIView* view in self->streamView.superview.subviews){ // iterates all on-screen widget views in StreamFrameView
|
||||
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
|
||||
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
|
||||
if(widgetView.pressed){
|
||||
gotOneButtonPressed = true; //got one button pressed
|
||||
}
|
||||
}
|
||||
}
|
||||
return gotOneButtonPressed;
|
||||
}
|
||||
|
||||
- (void)resetAllPressedFlagsForOnScreenWidgetViews {
|
||||
for(UIView* view in self->streamView.superview.subviews){ // iterates all on-screen widget views in StreamFrameView
|
||||
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
|
||||
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
|
||||
widgetView.pressed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)mouseRightClick {
|
||||
multiTouchesDetected = false;
|
||||
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC));
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
Log(LOG_D, @"Sending right mouse button press");
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
Log(LOG_D, @"Sending right mouse button press");
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -130,35 +116,34 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
||||
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) <= 300;
|
||||
}
|
||||
|
||||
- (BOOL)isAdjacentPoints:(CGPoint)currentPoint from:(CGPoint)originalPoint tolerance:(CGFloat)tolerance {
|
||||
bool isAdjacent = hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) <= hypot(tolerance, tolerance);
|
||||
return isAdjacent;
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
firstTouchMoved = false;
|
||||
|
||||
//check if touch point is spawned on the left or right upper half screen edges, this is the highest priority
|
||||
CGPoint initialPoint = [[touches anyObject] locationInView:streamView];
|
||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < _edgeTolerance || initialPoint.x > screenWidthWithThreshold)) {
|
||||
self->touchPointSpawnedAtUpperScreenEdge = true;
|
||||
return;
|
||||
}
|
||||
|
||||
firstTouchMoved = false;
|
||||
touchPointSpawnedAtUpperScreenEdge = false; // reset this flag immediately if we get a touch event passing the check above, this fixes irresponsive touch after closing the command tool menu.
|
||||
|
||||
if([[event allTouches] count] == 2 && ![self isOnScreenWidgetViewBeingPressed] && ![self isOnScreenControllerBeingPressed:[event allTouches]]){
|
||||
NSLog(@"get in scrolling mode");
|
||||
isInMouseWheelScrollingMode = true;
|
||||
return; // if we got 2 touches on the blank area, it's gonna be a mouse scroll touch, and must prevent UITtouch object for mouse pointer being captured & locked
|
||||
if([UITouchUtil touchesIn:streamView from:event].count>=2){
|
||||
multiTouchesDetected = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// NSLog(@"touches count in began stage: %llu", (uint64_t)[touches count]);
|
||||
|
||||
UITouch* candidateTouch = nil;
|
||||
|
||||
// the onscreen controllers are implmented by CALayer, which can not intercept UITouch event, touch will penetrate to the streamView level and captured in the touches callback of this touchHandler class.
|
||||
// the onscreen button are UIViews, they intercept UITouch events, so we don't need to worry about them.
|
||||
for(UITouch* touch in touches){
|
||||
// NSLog(@"candidate touch test: %llu", (uint64_t)touch);
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]){
|
||||
// NSLog(@"%f controller tap detected", CACurrentMediaTime());
|
||||
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]){
|
||||
continue;
|
||||
}
|
||||
else candidateTouch = touch;
|
||||
@@ -166,15 +151,18 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
||||
|
||||
// quick double tap detection for dragging. simulates a real notebook computer touchpad
|
||||
CGPoint currentTouchLocation = [candidateTouch locationInView:streamView];
|
||||
NSTimeInterval tapInterval = CACurrentMediaTime() - mousePointerTimestamp;
|
||||
if(tapInterval < QUICK_TAP_TIME_INTERVAL && [self isAdjacentTouches:currentTouchLocation from:initialMousePointerLocation] ) {
|
||||
// NSLog(@"quick click detected");
|
||||
quickTapDetected = true;
|
||||
NSLog(@"quick Tap Detected");
|
||||
// LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); // do not press down mouse button here, or it wiil easily turn to double click on remote PC
|
||||
|
||||
if([UITouchUtil touchesIn:streamView from:event].count == 1){
|
||||
NSTimeInterval tapInterval = CACurrentMediaTime() - mousePointerTimestamp;
|
||||
if(tapInterval < QUICK_TAP_TIME_INTERVAL
|
||||
&& [self isAdjacentTouches:currentTouchLocation from:initialMousePointerLocation]) {
|
||||
quickTapDetected = true;
|
||||
NSLog(@"quick Tap Detected");
|
||||
}
|
||||
quickTapTouch = touches.anyObject;
|
||||
}
|
||||
|
||||
// we must use [event allTouches] to check if touchLockedForMouseMove is captured, because the UITouch object could be captured by upper layer of UIView(in cases like tap gestures), not passed to the touches callbacks in this class, but still available in [event allTouches]
|
||||
// use [event allTouches] to check if touchLockedForMouseMove is captured, if already captured, don't update touchLockedForMouseMove
|
||||
if(candidateTouch != nil && ![[event allTouches] containsObject:touchLockedForMouseMove]){
|
||||
touchLockedForMouseMove = candidateTouch;
|
||||
// NSLog(@"Candidate touch for mouse movement locked");
|
||||
@@ -185,35 +173,35 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
||||
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
|
||||
//NSLog(@"%f, touchesMoved callback, is scrolling: %d, touches count: %d", CACurrentMediaTime(), isInMouseWheelScrollingMode, (uint32_t)[touches count]);
|
||||
NSSet* currentTouches = [UITouchUtil touchesIn:streamView from:event];
|
||||
|
||||
if(isInMouseWheelScrollingMode && [[event allTouches] count] == 2 && ![self isOnScreenWidgetViewBeingPressed] && ![self isOnScreenControllerBeingPressed:touches]){
|
||||
NSSet* twoTouches = [event allTouches];
|
||||
CGPoint firstLocation = [[[twoTouches allObjects] objectAtIndex:0] locationInView:streamView];
|
||||
CGPoint secondLocation = [[[twoTouches allObjects] objectAtIndex:1] locationInView:streamView];
|
||||
if(![self isOnScreenControllerBeingPressed:currentTouches]) [TouchPadGestureHandler handleGestureIn:streamView with:event];
|
||||
|
||||
CGPoint avgLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2);
|
||||
if ((CACurrentMediaTime() - _mouseRightClickTapRecognizer.gestureCapturedTime > RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S) && twoFingerTouchLocation.y != avgLocation.y) { //prevent sending scrollevent while right click gesture is being recognized. The time threshold is only 150ms, resulting in a barely noticeable delay before the scroll event is activated.
|
||||
// and we must exclude onscreen button taps & on-screen controller taps
|
||||
LiSendHighResScrollEvent((avgLocation.y - twoFingerTouchLocation.y) * 10);
|
||||
}
|
||||
twoFingerTouchLocation = avgLocation;
|
||||
return;
|
||||
}
|
||||
|
||||
// NSLog(@"%f touchesMoved callback, locked touch: %llu", CACurrentMediaTime(), (uintptr_t)touchLockedForMouseMove);
|
||||
if(multiTouchesDetected) return;
|
||||
|
||||
if([touches containsObject:touchLockedForMouseMove]){
|
||||
mousePointerMoved = true;
|
||||
[self sendMouseMoveEvent:touchLockedForMouseMove];
|
||||
CGPoint currentLocation = [touchLockedForMouseMove locationInView:streamView];
|
||||
bool isAdjacentPoints = [self isAdjacentPoints:initialMousePointerLocation from:currentLocation tolerance:currentSettings.singleTapSensitivity.doubleValue];
|
||||
if(!mousePointerMoved && !isAdjacentPoints){
|
||||
mousePointerMoved = true;
|
||||
}
|
||||
[self sendMouseMoveEvent:currentLocation];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if(isInMouseWheelScrollingMode){
|
||||
isInMouseWheelScrollingMode = false;
|
||||
|
||||
if(TouchPadGestureHandler.ctrlDown) LiSendKeyboardEvent(CommandManager.keyboardButtonMappings[@"CTRL"].shortValue,KEY_ACTION_UP,0);
|
||||
|
||||
[TouchPadGestureHandler startInertialScroll];
|
||||
|
||||
if(multiTouchesDetected){
|
||||
if([UITouchUtil touchesIn:streamView from:event].count == touches.count){
|
||||
multiTouchesDetected = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if([UITouchUtil touchesIn:streamView from:event].count == touches.count) multiTouchesDetected = false;
|
||||
|
||||
if([touches containsObject:touchLockedForMouseMove]){
|
||||
// dealing with a single first tap, whether the button will be released, is going to be decided in sendLongMouseLeftButtonClickEvent
|
||||
@@ -230,24 +218,23 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
||||
mousePointerMoved = false;
|
||||
}
|
||||
|
||||
if([[event allTouches] count] == [touches count]){
|
||||
isInMouseWheelScrollingMode = false;
|
||||
for(UITouch* touch in touches){
|
||||
[OnScreenControls.touchesCapturedByOnScreenControls removeObject:touch];
|
||||
}
|
||||
|
||||
if([UITouchUtil touchesIn:streamView from:event].count == [touches count]){
|
||||
touchLockedForMouseMove = nil;
|
||||
mousePointerMoved = false; // need to reset this anyway
|
||||
[self resetAllPressedFlagsForOnScreenWidgetViews]; // reset all pressed flag for on-screen widget views after all fingers lifted from screen.
|
||||
}
|
||||
|
||||
touchPointSpawnedAtUpperScreenEdge = false;
|
||||
}
|
||||
|
||||
|
||||
- (void)sendMouseMoveEvent:(UITouch* )touch{
|
||||
- (void)sendMouseMoveEvent:(CGPoint)currentLocation{
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
if(self->touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
||||
bool isAdjacentPoints = [self isAdjacentPoints:self->initialMousePointerLocation from:currentLocation tolerance:self->currentSettings.relativeTouchSlideThreshold.floatValue];
|
||||
|
||||
CGPoint currentLocation = [touch locationInView:self->streamView];
|
||||
|
||||
if (!self->firstTouchMoved) {
|
||||
if (!self->firstTouchMoved && !isAdjacentPoints) {
|
||||
self->latestMousePointerLocation = currentLocation;
|
||||
self->firstTouchMoved = true;
|
||||
}
|
||||
@@ -259,37 +246,40 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
||||
int deltaY = (currentLocation.y - self->latestMousePointerLocation.y) * 1.35 * self->currentSettings.mousePointerVelocityFactor.floatValue;
|
||||
|
||||
if (deltaX != 0 || deltaY != 0) {
|
||||
LiSendMouseMoveEvent(deltaX, deltaY);
|
||||
self->latestMousePointerLocation = currentLocation;
|
||||
if(self->touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
||||
if(self->firstTouchMoved) LiSendMouseMoveEvent(deltaX, deltaY);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// this will turn into a dragging anytime...
|
||||
- (void)sendLongMouseLeftButtonClickEvent{
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
// if (!self->isDragging){
|
||||
Log(LOG_D, @"Sending left mouse button press");
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(QUICK_TAP_TIME_INTERVAL * 1000000);
|
||||
if(!self->quickTapDetected){
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
}
|
||||
// else NSLog(@"Left mouse button release cancelled, keep pressing down, turning into dragging...");
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(QUICK_TAP_TIME_INTERVAL * NSEC_PER_SEC));
|
||||
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
if(!self->quickTapDetected){
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
}
|
||||
// else NSLog(@"Left mouse button release cancelled, keep pressing down, turning into dragging...");
|
||||
});
|
||||
// do not release the button if we're still dragging, this will prevent the dragging from being interrupted.
|
||||
});
|
||||
}
|
||||
|
||||
- (void)sendShortMouseLeftButtonClickEvent{
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
usleep(50 * 1000);
|
||||
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC));
|
||||
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
usleep(50 * 1000);
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// SafeTimer.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025/9/9.
|
||||
// Copyright © 2025 True砖家@Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import QuartzCore
|
||||
|
||||
@objc(SafeTimer)
|
||||
final class SafeTimer: NSObject {
|
||||
private var timer: DispatchSourceTimer?
|
||||
private let timerQueue: DispatchQueue // 私有串行队列
|
||||
private let userHandler: () -> Void
|
||||
private let interval: TimeInterval
|
||||
private let delay: TimeInterval
|
||||
|
||||
private var shouldRunHandler: Bool = false // 控制逻辑上的暂停/恢复
|
||||
private var isCleaned: Bool = false // 是否已经 clean
|
||||
|
||||
@objc init(interval: TimeInterval = 1.0,
|
||||
delay: TimeInterval = 0,
|
||||
queueLabel: String = "com.example.safetimer",
|
||||
handler: @escaping () -> Void) {
|
||||
|
||||
self.interval = interval
|
||||
self.delay = delay
|
||||
self.userHandler = handler
|
||||
self.timerQueue = DispatchQueue(label: queueLabel)
|
||||
|
||||
super.init()
|
||||
|
||||
let t = DispatchSource.makeTimerSource(queue: timerQueue)
|
||||
t.schedule(deadline: .now() + delay, repeating: interval)
|
||||
|
||||
t.setEventHandler { [weak self] in
|
||||
guard let self = self, !self.isCleaned else { return }
|
||||
if !self.shouldRunHandler { return }
|
||||
self.userHandler()
|
||||
}
|
||||
|
||||
t.resume() // timer 一开始就 resume,避免 suspend/resume 的计数问题
|
||||
self.timer = t
|
||||
}
|
||||
|
||||
deinit {
|
||||
clean()
|
||||
}
|
||||
|
||||
/// 开始逻辑上的计时
|
||||
@objc func start() {
|
||||
timerQueue.async { [weak self] in
|
||||
guard let self = self, !self.isCleaned else { return }
|
||||
self.shouldRunHandler = true
|
||||
}
|
||||
}
|
||||
|
||||
/// 暂停逻辑上的计时
|
||||
@objc func pause() {
|
||||
timerQueue.async { [weak self] in
|
||||
guard let self = self, !self.isCleaned else { return }
|
||||
self.shouldRunHandler = false
|
||||
}
|
||||
}
|
||||
|
||||
/// 重置下一次触发时间,并开始执行 handler
|
||||
@objc func restart() {
|
||||
timerQueue.async { [weak self] in
|
||||
guard let self = self, let t = self.timer, !self.isCleaned else { return }
|
||||
t.schedule(deadline: .now() + self.delay, repeating: self.interval)
|
||||
self.shouldRunHandler = true
|
||||
}
|
||||
}
|
||||
|
||||
@objc func isRunning() -> Bool {
|
||||
return shouldRunHandler;
|
||||
}
|
||||
|
||||
/// 彻底清理 timer,返回后保证 handler 不再执行
|
||||
@objc func clean() {
|
||||
if isCleaned { return }
|
||||
// 同步在 timerQueue 执行,保证已排队事件全部处理完(或跳过)
|
||||
timerQueue.sync {
|
||||
self.isCleaned = true
|
||||
self.shouldRunHandler = false
|
||||
|
||||
if let t = self.timer {
|
||||
t.setEventHandler {} // 清空 handler
|
||||
t.cancel() // 取消 timer
|
||||
self.timer = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
- (void)toggleStatsOverlay;
|
||||
- (void)toggleMouseCapture;
|
||||
- (void)toggleMouseVisible;
|
||||
- (void)disconnectAndQuitApp;
|
||||
|
||||
@end
|
||||
|
||||
@@ -33,18 +34,24 @@
|
||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate>
|
||||
#endif
|
||||
|
||||
@property (assign, nonatomic) UIView* streamFrameTopLayerView;
|
||||
@property (weak, nonatomic) UIView* streamFrameTopLayerView;
|
||||
@property (weak, nonatomic) id<UserInteractionDelegate> interactionDelegate;
|
||||
@property (assign, nonatomic) CGFloat streamAspectRatio;
|
||||
@property (assign, nonatomic) CGRect originalFrame;
|
||||
@property (assign, nonatomic) bool widgetToolOpened;
|
||||
|
||||
@property (strong, nonatomic) OnScreenControls* onScreenControls;
|
||||
@property (weak, nonatomic) PencilHandler* pencilHandler;
|
||||
@property (weak, nonatomic) UIViewController* streamFrameVC;
|
||||
|
||||
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
||||
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
|
||||
config:(StreamConfiguration*)streamConfig
|
||||
streamFrameTopLayerView:(UIView* )topLayerView
|
||||
;
|
||||
- (void) showOnScreenControls;
|
||||
|
||||
- (void)cleanUp;
|
||||
|
||||
- (void) reloadLegacyWidgets:(OSCProfile* )profile;
|
||||
- (void) setOnScreenControls;
|
||||
- (void) disableOnScreenControls;
|
||||
- (void) reloadOnScreenControlsRealtimeWith:(ControllerSupport*)controllerSupport
|
||||
@@ -52,7 +59,9 @@
|
||||
- (void) reloadOnScreenControlsWith:(ControllerSupport*)controllerSupport
|
||||
andConfig:(StreamConfiguration*)streamConfig;
|
||||
- (void) clearOnScreenWidgets;
|
||||
- (void) reloadOnScreenWidgetViews;
|
||||
- (void) reloadOnScreenWidgetViews:(bool)reload;
|
||||
- (void) saveStreamViewWidgetChanges;
|
||||
- (bool) isOnScreenWidgetEnabled;
|
||||
|
||||
- (CGSize) getVideoAreaSize;
|
||||
- (CGPoint) adjustCoordinatesForVideoArea:(CGPoint)point;
|
||||
@@ -60,11 +69,17 @@
|
||||
|
||||
- (OnScreenControlsLevel) getCurrentOscState;
|
||||
|
||||
-(void)readyToBringUpSoftKeyboardByToolbox;
|
||||
- (void)readyToBringUpSoftKeyboardByToolbox;
|
||||
- (void)keyboardWillShow:(NSNotification *)notification;
|
||||
- (void)keyboardWillHide;
|
||||
- (void)liftMetalVideoViewIfNeeded:(CGFloat)liftHeight;
|
||||
|
||||
- (void)alterAbsTouchDragWith:(int32_t)mouseButton;
|
||||
|
||||
- (void)enablePencilHover;
|
||||
- (void)disablePencilHover;
|
||||
- (void)setAllowSingleTouchEnabled:(BOOL)enabled;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
|
||||
#endif
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#import "KeyboardInputField.h"
|
||||
#import "CustomTapGestureRecognizer.h"
|
||||
#import "LocalizationHelper.h"
|
||||
#import "StreamFrameViewController.h"
|
||||
|
||||
|
||||
static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
@@ -34,18 +35,15 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
Stream Video has been moved out of this class to _renderView in StreamFrameViewController.
|
||||
*/
|
||||
@implementation StreamView {
|
||||
UIView* streamFrameTopLayerView;
|
||||
TemporarySettings* settings;
|
||||
|
||||
OnScreenControls* onScreenControls;
|
||||
TemporarySettings* settings;
|
||||
|
||||
KeyboardInputField* keyInputField;
|
||||
BOOL isInputingText;
|
||||
bool isPencilHovering;
|
||||
NSMutableSet* keysDown;
|
||||
float streamAspectRatio;
|
||||
|
||||
|
||||
|
||||
// iOS 13.4 mouse support
|
||||
NSInteger lastMouseButtonMask;
|
||||
float lastMouseX;
|
||||
@@ -61,7 +59,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
|
||||
UIResponder* touchHandler;
|
||||
|
||||
id<UserInteractionDelegate> interactionDelegate;
|
||||
NSTimer* interactionTimer;
|
||||
BOOL hasUserInteracted;
|
||||
|
||||
@@ -76,6 +73,9 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
UILabel* keyboardToggleTip;
|
||||
|
||||
UIKeyModifierFlags comboKeyModifierFlags;
|
||||
|
||||
WidgetSizeTransition _widgetSizeTransition;
|
||||
OSCProfilesManager* oscProfileMan;
|
||||
}
|
||||
|
||||
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
||||
@@ -84,11 +84,11 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
streamFrameTopLayerView:(UIView* )topLayerView{
|
||||
self->comboKeyModifierFlags = (UIKeyModifierControl|UIKeyModifierAlternate|UIKeyModifierShift);
|
||||
|
||||
self->streamFrameTopLayerView = topLayerView;
|
||||
self.streamFrameTopLayerView = topLayerView; // this will be used as a read-only pointer for other class
|
||||
self->interactionDelegate = interactionDelegate;
|
||||
self->_streamFrameTopLayerView = topLayerView;
|
||||
self->_interactionDelegate = interactionDelegate;
|
||||
self->streamAspectRatio = (float)streamConfig.width / (float)streamConfig.height;
|
||||
self.streamAspectRatio = self->streamAspectRatio;
|
||||
_widgetSizeTransition = keepWidgetSize;
|
||||
|
||||
settings = [[[DataManager alloc] init] getSettings];
|
||||
|
||||
@@ -115,7 +115,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
keyboardToggleTip.numberOfLines = 1;
|
||||
keyboardToggleTip.layer.cornerRadius = 10;
|
||||
keyboardToggleTip.clipsToBounds = true;
|
||||
NSLog(@"setup streamView %f", CACurrentMediaTime());
|
||||
|
||||
// if(settings.touchMode.intValue == NativeTouchOnly) [self addGestureRecognizer:keyboardToggleRecognizer]; //keep legacy approach in pure native mode
|
||||
// else [self->streamFrameTopLayerView addGestureRecognizer:keyboardToggleRecognizer]; //add to the superview in other modes
|
||||
@@ -126,12 +125,17 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
// tvOS requires RelativeTouchHandler to manage Apple Remote input
|
||||
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
|
||||
#else
|
||||
|
||||
PencilHandler.shared = [[PencilHandler alloc] initWithStreamView:self settings:settings];
|
||||
_pencilHandler = PencilHandler.shared;
|
||||
|
||||
// iOS uses touch Mode depending on user preference
|
||||
|
||||
switch (settings.touchMode.intValue) {
|
||||
case NativeTouch:
|
||||
keyboardToggleRecognizer.immediateTriggering = false;
|
||||
self->touchHandler = [[NativeTouchHandler alloc] initWithView:self andSettings:settings];break;
|
||||
self->touchHandler = [[NativeTouchHandler alloc] initWithView:self andSettings:settings];
|
||||
break;
|
||||
case NativeTouchOnly:
|
||||
keyboardToggleRecognizer.immediateTriggering = false;
|
||||
self->touchHandler = [[PureNativeTouchHandler alloc] initWithView:self andSettings:settings];break;
|
||||
@@ -141,16 +145,21 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
// if(settings.onscreenControls.intValue == OnScreenControlsLevelCustom) keyboardToggleRecognizer.numberOfTouchesRequired = 3; //deprecated: fixing keyboard taps to 3, in order to invoke OSC rebase in stream view by 4-finger tap.
|
||||
break;
|
||||
case AbsoluteTouch:
|
||||
self->touchHandler = [[AbsoluteTouchHandler alloc] initWithView:self];
|
||||
self->touchHandler = [[AbsoluteTouchHandler alloc] initWithView:self andSettings:settings];
|
||||
keyboardToggleRecognizer.immediateTriggering = true; //triggers signal in touchesBegan callback stage
|
||||
break;
|
||||
case TouchDisabled:
|
||||
self->touchHandler = nil;
|
||||
keyboardToggleRecognizer.immediateTriggering = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// we'll render on-screen controls on the toplayer too:
|
||||
onScreenControls = [[OnScreenControls alloc] initWithView:self->streamFrameTopLayerView controllerSup:controllerSupport streamConfig:streamConfig]; // don't delete, this is mandatory
|
||||
_onScreenControls = [[OnScreenControls alloc] initWithView:self->_streamFrameTopLayerView controllerSup:controllerSupport streamConfig:streamConfig]; // don't delete, this is mandatory
|
||||
// OnScreenControls.shared = _onScreenControls;
|
||||
/*
|
||||
// here we pass the tap recognizer to the onscreencontrols obj
|
||||
if (settings.touchMode.intValue == RelativeTouch){
|
||||
@@ -161,10 +170,10 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue];
|
||||
if (settings.touchMode.intValue != RelativeTouch && settings.touchMode.intValue != NativeTouch ) {
|
||||
Log(LOG_I, @"On-screen controls disabled in non-relative touch mode");
|
||||
[onScreenControls setLevel:OnScreenControlsLevelOff];
|
||||
[_onScreenControls setLevel:OnScreenControlsLevelOff];
|
||||
|
||||
//pass touchesCaptureByOnScreenButtons Set to the native touchhandler, this NSSet is init witihin onscreencontrols class, don't do it again in native touch handler class
|
||||
[OnScreenControls.touchAddrsCapturedByOnScreenControls removeAllObjects]; // reset the attribute to nil
|
||||
[OnScreenControls.touchesCapturedByOnScreenControls removeAllObjects]; // reset the attribute to nil
|
||||
|
||||
/*
|
||||
if(settings.touchMode.intValue == NativeTouch){
|
||||
@@ -176,11 +185,11 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
}
|
||||
//else if (level == OnScreenControlsLevelAuto) {
|
||||
else if (false) { // level auto cancelled in settings
|
||||
[controllerSupport initAutoOnScreenControlMode:onScreenControls];
|
||||
[controllerSupport initAutoOnScreenControlMode:_onScreenControls];
|
||||
}
|
||||
else {
|
||||
Log(LOG_I, @"Setting manual on-screen controls level: %d", (int)level);
|
||||
[onScreenControls setLevel:level];
|
||||
[_onScreenControls setLevel:level];
|
||||
}
|
||||
// It would be nice to just use GCMouse on iOS 14+ and the older API on iOS 13
|
||||
// but unfortunately that isn't possible today. GCMouse doesn't recognize many
|
||||
@@ -200,8 +209,11 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
continuousMouseWheelRecognizer.allowedScrollTypesMask = UIScrollTypeMaskContinuous;
|
||||
continuousMouseWheelRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)];
|
||||
[self addGestureRecognizer:continuousMouseWheelRecognizer];
|
||||
|
||||
isPencilHovering = false;
|
||||
}
|
||||
|
||||
|
||||
#if defined(__IPHONE_16_1) || defined(__TVOS_16_1)
|
||||
if (@available(iOS 16.1, *)) {
|
||||
UIHoverGestureRecognizer *stylusHoverRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(sendStylusHoverEvent:)];
|
||||
@@ -231,17 +243,36 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
keyboardToggleRecognizer.delaysTouchesBegan = NO;
|
||||
keyboardToggleRecognizer.delaysTouchesEnded = NO;
|
||||
[self->_streamFrameTopLayerView addGestureRecognizer:keyboardToggleRecognizer];
|
||||
keyboardToggleRecognizer.touchCapturingView = self;
|
||||
}
|
||||
|
||||
- (void)keyboardWillShow:(NSNotification *)notification{
|
||||
// NSLog(@"keyboard will show markmark %f", CACurrentMediaTime());
|
||||
if(settings.liftStreamViewForKeyboard && !isInputingText){
|
||||
isInputingText = true;
|
||||
|
||||
NSDictionary *userInfo = notification.userInfo;
|
||||
// Get the keyboard size from the notification
|
||||
CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||
// NSLog(@"keyboard will show markmark, lowest height %f", keyboardToggleRecognizer.lowestTouchPointHeight);
|
||||
if(keyboardFrame.size.height < CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.25) return; // return in case of abnormal keyboard height
|
||||
HeightViewLiftedTo = keyboardFrame.size.height - keyboardToggleRecognizer.lowestTouchPointHeight + CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.1; // lift the StreamView to the height of lowest touch point of multi-finger tap gesture, while reserving the view of 1/10 screen height for remote typing.
|
||||
CGFloat screenHeight = CGRectGetHeight([[UIScreen mainScreen] bounds]);
|
||||
CGFloat keyboardHeight = keyboardFrame.size.height;
|
||||
CGFloat toolbarHeight = settings.showKeyboardToolbar ? GenericUtils.legacyToolbarHeight : 0;
|
||||
|
||||
if(keyboardHeight < screenHeight * 0.33333333333 + toolbarHeight
|
||||
|| keyboardHeight > screenHeight*0.8 + toolbarHeight){
|
||||
keyboardHeight = screenHeight*0.5 + toolbarHeight;
|
||||
// [self toggleKeyboard];
|
||||
/*
|
||||
if(keyboardToggleRecognizer.lowestTouchPointHeight < screenHeight/2){
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)),
|
||||
dispatch_get_main_queue(), ^{
|
||||
[self toggleKeyboard];
|
||||
});
|
||||
return; // return in case of abnormal keyboard height
|
||||
}*/
|
||||
}
|
||||
|
||||
HeightViewLiftedTo = keyboardHeight - keyboardToggleRecognizer.lowestTouchPointHeight + CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.15; // lift the StreamView to the height of lowest touch point of multi-finger tap gesture, while reserving the view of 1/10 screen height for remote typing.
|
||||
if(HeightViewLiftedTo < 0) HeightViewLiftedTo = 0; // set HeightViewLiftedTo to 0 if it is high enough and not going to be covered by keyboard.
|
||||
CGRect liftedStreamFrame = self.frame;
|
||||
liftedStreamFrame.origin.y -= HeightViewLiftedTo;
|
||||
@@ -250,13 +281,23 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
// Also lift Metal video view if using Metal rendering backend
|
||||
[self liftMetalVideoViewIfNeeded:HeightViewLiftedTo];
|
||||
|
||||
isInputingText = true;
|
||||
[self refreshKeyboardToggleRecognizer:settings.keyboardToggleFingers.intValue];
|
||||
[keyboardToggleTip removeFromSuperview];
|
||||
}
|
||||
NSLog(@"keyboard will show %f", CACurrentMediaTime());
|
||||
}
|
||||
|
||||
- (UIViewController *)parentViewController {
|
||||
UIResponder *responder = self;
|
||||
while (responder) {
|
||||
if ([responder isKindOfClass:[UIViewController class]]) {
|
||||
return (UIViewController *)responder;
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// this method also deals with recovering streamview when local keyboard is turned off
|
||||
- (void)keyboardWillHide{
|
||||
// NSLog(@"keyboard will hide markmark %f", CACurrentMediaTime());
|
||||
@@ -329,6 +370,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
if (isInputingText) {
|
||||
Log(LOG_D, @"Closing the keyboard");
|
||||
[keyInputField resignFirstResponder];
|
||||
[keyboardToggleTip removeFromSuperview];
|
||||
} else {
|
||||
Log(LOG_D, @"Opening the keyboard");
|
||||
// Prepare the textbox used to capture keyboard events.
|
||||
@@ -337,18 +379,35 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
#if !TARGET_OS_TV
|
||||
// Prepare the toolbar above the keyboard for more options
|
||||
if(settings.showKeyboardToolbar){
|
||||
UIToolbar *customToolbarView = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, 44)];
|
||||
UIBarButtonItem *doneBarButton = [self createButtonWithImageNamed:@"DoneIcon.png" backgroundColor:[UIColor clearColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x00 isToggleable:NO];
|
||||
UIBarButtonItem *windowsBarButton = [self createButtonWithImageNamed:@"WindowsIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x5B isToggleable:YES];
|
||||
UIBarButtonItem *tabBarButton = [self createButtonWithImageNamed:@"TabIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x09 isToggleable:NO];
|
||||
UIBarButtonItem *shiftBarButton = [self createButtonWithImageNamed:@"ShiftIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0xA0 isToggleable:YES];
|
||||
UIBarButtonItem *escapeBarButton = [self createButtonWithImageNamed:@"EscapeIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x1B isToggleable:NO];
|
||||
UIBarButtonItem *controlBarButton = [self createButtonWithImageNamed:@"ControlIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x11 isToggleable:YES];
|
||||
UIBarButtonItem *altBarButton = [self createButtonWithImageNamed:@"AltIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x12 isToggleable:YES];
|
||||
UIBarButtonItem *deleteBarButton = [self createButtonWithImageNamed:@"DeleteIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x2E isToggleable:NO];
|
||||
UIToolbar *customToolbarView = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, GenericUtils.legacyToolbarHeight)];
|
||||
UIBarButtonItem *doneBarButton = [self createButtonWithImageNamed:@"DoneIcon.png" backgroundColor:[UIColor clearColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x00 isToggleable:NO isDoneButton:true];
|
||||
UIBarButtonItem *windowsBarButton = [self createButtonWithImageNamed:@"WindowsIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x5B isToggleable:YES isDoneButton:false];
|
||||
UIBarButtonItem *tabBarButton = [self createButtonWithImageNamed:@"TabIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x09 isToggleable:NO isDoneButton:false];
|
||||
UIBarButtonItem *shiftBarButton = [self createButtonWithImageNamed:@"ShiftIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0xA0 isToggleable:YES isDoneButton:false];
|
||||
UIBarButtonItem *escapeBarButton = [self createButtonWithImageNamed:@"EscapeIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x1B isToggleable:NO isDoneButton:false];
|
||||
UIBarButtonItem *controlBarButton = [self createButtonWithImageNamed:@"ControlIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x11 isToggleable:YES isDoneButton:false];
|
||||
UIBarButtonItem *altBarButton = [self createButtonWithImageNamed:@"AltIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x12 isToggleable:YES isDoneButton:false];
|
||||
UIBarButtonItem *deleteBarButton = [self createButtonWithImageNamed:@"DeleteIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x2E isToggleable:NO isDoneButton:false];
|
||||
UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
|
||||
|
||||
[customToolbarView setItems:[NSArray arrayWithObjects:doneBarButton, windowsBarButton, escapeBarButton, tabBarButton, shiftBarButton, controlBarButton, altBarButton, deleteBarButton, flexibleSpace, nil]];
|
||||
if (GenericUtils.liquidGlassEnabled) {
|
||||
if (@available(iOS 26.0, *)) {
|
||||
for(UIBarButtonItem *button in customToolbarView.items){
|
||||
button.hidesSharedBackground = true;
|
||||
}
|
||||
// customToolbarView.barTintColor = UIColor.systemGrayColor;
|
||||
customToolbarView.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.3];
|
||||
customToolbarView.layer.cornerRadius = customToolbarView.bounds.size.height/2;
|
||||
customToolbarView.layer.masksToBounds = true;
|
||||
|
||||
UIVisualEffectView *glassView =
|
||||
[[UIVisualEffectView alloc] initWithEffect:[UIGlassEffect effectWithStyle:UIGlassEffectStyleClear]];
|
||||
glassView.frame = customToolbarView.bounds;
|
||||
glassView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
[customToolbarView addSubview:glassView];
|
||||
[customToolbarView sendSubviewToBack:glassView];
|
||||
}
|
||||
}
|
||||
keyInputField.inputAccessoryView = customToolbarView;
|
||||
}
|
||||
#endif
|
||||
@@ -377,7 +436,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
|
||||
// Notify the delegate if this was a new user interaction
|
||||
if (!timerAlreadyRunning) {
|
||||
[interactionDelegate userInteractionBegan];
|
||||
[_interactionDelegate userInteractionBegan];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,7 +444,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
if (!hasUserInteracted) {
|
||||
// User has finished touching the screen
|
||||
interactionTimer = nil;
|
||||
[interactionDelegate userInteractionEnded];
|
||||
[_interactionDelegate userInteractionEnded];
|
||||
}
|
||||
else {
|
||||
// User is still touching the screen. Restart the timer.
|
||||
@@ -393,9 +452,9 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) showOnScreenControls {
|
||||
- (void) reloadLegacyWidgets:(OSCProfile* )profile {
|
||||
#if !TARGET_OS_TV
|
||||
if(!_widgetToolOpened) [onScreenControls show];
|
||||
if(!_widgetToolOpened) [_onScreenControls showLegacyWidgetsWith:profile];
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -405,34 +464,35 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
|
||||
- (void) disableOnScreenControls {
|
||||
#if !TARGET_OS_TV
|
||||
[onScreenControls setLevel:OnScreenControlsLevelOff];
|
||||
[_onScreenControls setLevel:OnScreenControlsLevelOff];
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
// we'll enable on screen buttons, and disable on screen controllers for absolute touch
|
||||
- (bool) isOscEnabled{
|
||||
return (settings.touchMode.intValue == RelativeTouch || settings.touchMode.intValue == NativeTouch) && settings.onscreenControls.intValue != OnScreenControlsLevelOff;
|
||||
return (settings.touchMode.intValue == RelativeTouch || settings.touchMode.intValue == NativeTouch || settings.touchMode.intValue == AbsoluteTouch || settings.touchMode.intValue == TouchDisabled) && settings.onscreenControls.intValue != OnScreenControlsLevelOff;
|
||||
}
|
||||
|
||||
// we'll enable on screen buttons, and disable on screen controllers for absolute touch
|
||||
- (bool) isOnScreenButtonEnabled{
|
||||
return (settings.touchMode.intValue == RelativeTouch || settings.touchMode.intValue == NativeTouch || settings.touchMode.intValue == AbsoluteTouch) && settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
|
||||
- (bool) isOnScreenWidgetEnabled{
|
||||
return [self isOscEnabled] && settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
|
||||
}
|
||||
|
||||
- (void) reloadOnScreenControlsRealtimeWith:(ControllerSupport*)controllerSupport
|
||||
andConfig:(StreamConfiguration*)streamConfig {
|
||||
[self reloadOnScreenControlsWith:controllerSupport andConfig:streamConfig];
|
||||
if([self isOscEnabled]) [self showOnScreenControls];
|
||||
else [self disableOnScreenControls];
|
||||
/*
|
||||
StreamFrameViewController* vc = (StreamFrameViewController* )_streamFrameVC;
|
||||
if([self isOscEnabled]) [self reloadLegacyWidgets:nil];
|
||||
else [self disableOnScreenControls];*/
|
||||
}
|
||||
|
||||
|
||||
- (void) reloadOnScreenControlsWith:(ControllerSupport*)controllerSupport
|
||||
andConfig:(StreamConfiguration*)streamConfig {
|
||||
|
||||
// we'll render on-screen controllers on the toplayer too.
|
||||
onScreenControls = [[OnScreenControls alloc] initWithView:self->streamFrameTopLayerView controllerSup:controllerSupport streamConfig:streamConfig];
|
||||
_onScreenControls = [[OnScreenControls alloc] initWithView:self->_streamFrameTopLayerView controllerSup:controllerSupport streamConfig:streamConfig];
|
||||
/*
|
||||
// pass mouseRightClickTapRecognizer to onScreenControls obj here:
|
||||
if([self isOscEnabled]){
|
||||
@@ -440,12 +500,14 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
RelativeTouchHandler* relativeTouchHandler = (RelativeTouchHandler *)touchHandler;
|
||||
onScreenControls.mouseRightClickTapRecognizer = relativeTouchHandler.mouseRightClickTapRecognizer;
|
||||
} */
|
||||
if([self isOscEnabled]) [onScreenControls setLevel:(OnScreenControlsLevel)settings.onscreenControls.intValue];
|
||||
if([self isOscEnabled]) [_onScreenControls setLevel:(OnScreenControlsLevel)settings.onscreenControls.intValue];
|
||||
}
|
||||
|
||||
|
||||
- (void) clearOnScreenWidgets{
|
||||
for (UIView *subview in self->streamFrameTopLayerView.subviews) {
|
||||
OnScreenWidgetView.isTweakingHighlight = false;
|
||||
[OnScreenWidgetView clearMappings];
|
||||
for (UIView *subview in self->_streamFrameTopLayerView.subviews) {
|
||||
// 检查子视图是否是特定类型的实例
|
||||
if ([subview isKindOfClass:[OnScreenWidgetView class]]) {
|
||||
// 如果是,就添加到将要被移除的数组中
|
||||
@@ -456,8 +518,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
|
||||
- (CGPoint)denormalizeWidgetPosition:(CGPoint)position {
|
||||
if(position.x < 1.0 && position.y < 1.0){
|
||||
position.x = position.x * streamFrameTopLayerView.bounds.size.width;
|
||||
position.y = position.y * streamFrameTopLayerView.bounds.size.height;
|
||||
position.x = position.x * _streamFrameTopLayerView.bounds.size.width;
|
||||
position.y = position.y * _streamFrameTopLayerView.bounds.size.height;
|
||||
}
|
||||
else{
|
||||
NSLog(@"invalid coords");
|
||||
@@ -465,59 +527,203 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
return position;
|
||||
}
|
||||
|
||||
- (void) reloadOnScreenWidgetViews{
|
||||
- (WidgetSizeReference)getCurrentWidgetSizeReference{
|
||||
if(_widgetSizeTransition == keepWidgetSize) return longSide;
|
||||
else if(_widgetSizeTransition == transitionWithOrientation) return self.bounds.size.width > self.bounds.size.height ? longSide : shortSide;
|
||||
else return longSide;
|
||||
}
|
||||
|
||||
// NSLog(@"reload on screen keyboard buttons here");
|
||||
- (void)saveStreamViewWidgetChanges{
|
||||
/*
|
||||
NSMutableDictionary* relocatedWidgetDict = [NSMutableDictionary dictionary];
|
||||
|
||||
// remove all keyboard widget views first
|
||||
[self clearOnScreenWidgets];
|
||||
|
||||
// bool customOscEnabled = [self isOscEnabled] && settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
|
||||
|
||||
if(![self isOnScreenButtonEnabled]) return;
|
||||
|
||||
OSCProfilesManager* profilesManager = [OSCProfilesManager sharedManager: self.bounds];
|
||||
OSCProfile *oscProfile = [profilesManager getSelectedProfile]; //returns the currently selected OSCProfile
|
||||
|
||||
if(!OnScreenWidgetView.editMode){ // in edit mode, keyboard widget view will be updated within layoutool view controller.
|
||||
for (NSData *buttonStateEncoded in oscProfile.buttonStates) {
|
||||
OnScreenButtonState* buttonState = [profilesManager unarchiveButtonStateEncoded:buttonStateEncoded];
|
||||
if(buttonState.buttonType == CustomOnScreenWidget){
|
||||
OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithCmdString:buttonState.name buttonLabel:buttonState.alias shape:buttonState.widgetShape]; //reconstruct widgetView
|
||||
//--------------------------------------------------
|
||||
onScreenControls.delegate = widgetView; // connecting onScreenControls to OnScreenWidgetView, sending the active instance for touchPad stick control
|
||||
[onScreenControls sendInstance];
|
||||
//--------------------------------------------------
|
||||
widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place
|
||||
widgetView.widthFactor = buttonState.widthFactor;
|
||||
widgetView.heightFactor = buttonState.heightFactor;
|
||||
widgetView.borderWidth = buttonState.borderWidth;
|
||||
[widgetView setVibrationWithStyle:buttonState.vibrationStyle];
|
||||
widgetView.mouseButtonAction = buttonState.mouseButtonAction;
|
||||
widgetView.sensitivityFactorX = buttonState.sensitivityFactorX;
|
||||
widgetView.sensitivityFactorY = buttonState.sensitivityFactorY;
|
||||
widgetView.trackballDecelerationRate = buttonState.decelerationRate;
|
||||
widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset;
|
||||
widgetView.minStickOffset = buttonState.minStickOffset;
|
||||
widgetView.slideMode = buttonState.slideMode;
|
||||
// Add the widgetView to the view controller's view
|
||||
[self->streamFrameTopLayerView addSubview:widgetView]; // add keyboard button to the stream frame view. must add it to the target view before setting location.
|
||||
buttonState.position = [self denormalizeWidgetPosition:buttonState.position];
|
||||
[widgetView setLocationWithPosition:buttonState.position];
|
||||
[widgetView resizeWidgetView]; // resize must be called after relocation
|
||||
[widgetView adjustTransparencyWithAlpha:buttonState.backgroundAlpha];
|
||||
[widgetView adjustBorderWithWidth:buttonState.borderWidth];
|
||||
}
|
||||
for(UIView* subview in self->_streamFrameTopLayerView.subviews){
|
||||
if([subview isKindOfClass:[OnScreenWidgetView class]]){
|
||||
OnScreenWidgetView* widget = (OnScreenWidgetView* )subview;
|
||||
if(widget.relocatedDuringStreaming) [relocatedWidgetDict setObject:widget forKey:@(widget.sequence)];
|
||||
OnScreenWidgetView* widget = [OnScreenWidgetView widgetForKey:newButtonState.sequence];
|
||||
if(widget.isFolder)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
NSMutableSet* relocatedWidgetSequences = [NSMutableSet set];
|
||||
bool hasFolderStateChanged = false;
|
||||
for(OnScreenWidgetView* widget in [OnScreenWidgetView.mapping allValues]){
|
||||
if(widget.relocatedDuringStreaming) [relocatedWidgetSequences addObject:@(widget.sequence)];
|
||||
if(!hasFolderStateChanged) hasFolderStateChanged = widget.folded != widget.persistedFolded;
|
||||
}
|
||||
|
||||
if(relocatedWidgetSequences.count == 0 && !hasFolderStateChanged) return;
|
||||
NSLog(@"relocatedWidgetSequences.count %lu, hasFolderStateChanged %d %F", relocatedWidgetSequences.count, hasFolderStateChanged, CACurrentMediaTime());
|
||||
|
||||
oscProfileMan = [OSCProfilesManager sharedManager:self->_streamFrameTopLayerView.bounds];
|
||||
OSCProfile *newProfile = [oscProfileMan getSelectedProfile];
|
||||
|
||||
for (NSInteger i = 0; i < newProfile.buttonStatesEncoded.count; i++) {
|
||||
NSData* buttonStateEncoded = newProfile.buttonStatesEncoded[i];
|
||||
OnScreenButtonState *newButtonState = [oscProfileMan unarchiveButtonStateEncoded:buttonStateEncoded];
|
||||
OnScreenWidgetView* widget = [OnScreenWidgetView.mapping objectForKey:@(newButtonState.sequence)];
|
||||
NSData *newButtonStateEncoded;
|
||||
if([relocatedWidgetSequences containsObject:@(widget.sequence)]
|
||||
|| widget.isFolder){
|
||||
newButtonState.position = [oscProfileMan normalizeWidgetPosition:widget.storedCenter];
|
||||
newButtonState.folded = widget.folded;
|
||||
newButtonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:newButtonState requiringSecureCoding:YES error:nil];
|
||||
newProfile.buttonStatesEncoded[i] = newButtonStateEncoded;
|
||||
}
|
||||
}
|
||||
|
||||
newProfile.unfoldedExclusiveFolderSequence = OnScreenWidgetView.unfoldedExclusiveFolderSequence;
|
||||
newProfile.postExclusiveUnfoldedSequences = OnScreenWidgetView.postExclusiveUnfoldedSequences;
|
||||
|
||||
[oscProfileMan replaceSelectedProfileWith:newProfile overwriteDefault:YES];
|
||||
}
|
||||
|
||||
- (void) reloadOnScreenWidgetViews:(bool)reload{
|
||||
NSLog(@"reloadOnScreenWidgets in streamview %f", CACurrentMediaTime());
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if(!self->oscProfileMan) self->oscProfileMan = [OSCProfilesManager sharedManager:self->_streamFrameTopLayerView.bounds];
|
||||
OSCProfile *oscProfile = [self->oscProfileMan getSelectedProfile]; //returns the currently selected OSCProfile
|
||||
MotionHandler* motionHandler = [MotionHandler sharedWithProfile:oscProfile];
|
||||
|
||||
// get streamFrameVC
|
||||
if(!self->_streamFrameVC) self->_streamFrameVC = [self parentViewController];
|
||||
|
||||
// bool customOscEnabled = [self isOscEnabled] && settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
|
||||
|
||||
if(![self isOnScreenWidgetEnabled]) return;
|
||||
|
||||
OnScreenWidgetView.buttonVisualFeedbackEnabled = self->settings.buttonVisualFeedback;
|
||||
|
||||
bool hasLegacyWidget = false;
|
||||
if(reload && !OnScreenWidgetView.editMode){
|
||||
// remove all keyboard widget views first
|
||||
[self clearOnScreenWidgets];
|
||||
|
||||
bool sequenceGenerated = false;
|
||||
bool hasMovableWidget = false;
|
||||
for (NSInteger i = 0; i < oscProfile.buttonStatesEncoded.count; i++) {
|
||||
NSData* buttonStateEncoded = oscProfile.buttonStatesEncoded[i];
|
||||
OnScreenButtonState* buttonState = [self->oscProfileMan unarchiveButtonStateEncoded:buttonStateEncoded];
|
||||
if(buttonState.widgetType == CustomOnScreenWidget){
|
||||
OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithCmdString:buttonState.name buttonLabel:buttonState.alias shape:buttonState.widgetShape profile:oscProfile]; //reconstruct widgetView
|
||||
|
||||
widgetView.functionalButtonDelegate = (id<OnScreenFunctionalButtonDelegate>)self->_streamFrameVC;
|
||||
widgetView.motionHandler = motionHandler;
|
||||
|
||||
widgetView.sequence = buttonState.sequence == -1 ? i : buttonState.sequence;
|
||||
if(!sequenceGenerated) sequenceGenerated = buttonState.sequence == -1;
|
||||
if(!hasMovableWidget) hasMovableWidget = buttonState.buttonMode == movable;
|
||||
[OnScreenWidgetView setWithWidget:widgetView for:widgetView.sequence];
|
||||
widgetView.sequenceSet = buttonState.sequenceSet;
|
||||
widgetView.parentSequence = buttonState.parentSequence;
|
||||
widgetView.folded = buttonState.folded;
|
||||
widgetView.persistedFolded = buttonState.folded;
|
||||
widgetView.revealMode = buttonState.revealMode;
|
||||
|
||||
widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place
|
||||
widgetView.widthFactor = buttonState.widthFactor;
|
||||
widgetView.heightFactor = buttonState.heightFactor;
|
||||
widgetView.componentSizeFactor = buttonState.componentSizeFactor;
|
||||
widgetView.borderWidth = buttonState.borderWidth;
|
||||
widgetView.highlightSizeFactor = buttonState.highlightSizeFactor;
|
||||
widgetView.autoTapInterval = buttonState.autoTapInterval;
|
||||
[widgetView setVibrationWithStyle:buttonState.vibrationStyle];
|
||||
widgetView.mouseButtonAction = buttonState.mouseButtonAction;
|
||||
widgetView.sensitivityFactorX = buttonState.sensitivityFactorX;
|
||||
widgetView.sensitivityFactorY = buttonState.sensitivityFactorY;
|
||||
widgetView.slideThreshold = buttonState.slideThreshold;
|
||||
widgetView.yawFactor = buttonState.yawFactor;
|
||||
widgetView.pitchFactor = buttonState.pitchFactor;
|
||||
widgetView.rollFactor = buttonState.rollFactor;
|
||||
widgetView.decelerationRateX = buttonState.decelerationRateX;
|
||||
widgetView.decelerationRateY = buttonState.decelerationRateY;
|
||||
widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset;
|
||||
widgetView.minStickOffset = buttonState.minStickOffset;
|
||||
widgetView.dWheelWalkModeThreshold = buttonState.walkModeThreshold;
|
||||
widgetView.buttonMode = buttonState.buttonMode;
|
||||
// Add the widgetView to the view controller's view
|
||||
[self->_streamFrameTopLayerView addSubview:widgetView]; // add keyboard button to the stream frame view. must add it to the target view before setting location.
|
||||
buttonState.position = [self denormalizeWidgetPosition:buttonState.position];
|
||||
[widgetView setLocationWithPosition:buttonState.position];
|
||||
widgetView.sizeReference = [self getCurrentWidgetSizeReference];
|
||||
// widgetView.sizeReference = buttonState.sizeReference;
|
||||
//portrait markmarkmark
|
||||
[widgetView resizeWidgetView]; // resize must be called after relocation
|
||||
[widgetView adjustTransparencyWithAlpha:buttonState.backgroundAlpha tweakBorderAlpha:NO];
|
||||
[widgetView adjustBorderWithWidth:buttonState.borderWidth];
|
||||
[widgetView tweakLabelAlphaWithAlpha:buttonState.labelAlpha];
|
||||
[widgetView tweakBorderAlphaWithAlpha:buttonState.borderAlpha];
|
||||
[widgetView tweakHighlightAlphaWithAlpha:buttonState.highlightAlpha];
|
||||
[widgetView setupAutoTapTimer];
|
||||
[widgetView setupInertialScrollerWithFps:self->settings.framerate.intValue];
|
||||
|
||||
if(sequenceGenerated){
|
||||
// NSLog(@"widgetView.sequence %d %f", widgetView.sequence, CACurrentMediaTime());
|
||||
buttonState.sequence = widgetView.sequence;
|
||||
oscProfile.buttonStatesEncoded[i] = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
|
||||
}
|
||||
|
||||
[widgetView accessWidgetAttributes];
|
||||
}
|
||||
else if(buttonState.widgetType == LegacyOnScreenControls) hasLegacyWidget = true;
|
||||
}
|
||||
|
||||
if(sequenceGenerated && hasMovableWidget){
|
||||
[self->oscProfileMan replaceSelectedProfileWith:oscProfile overwriteDefault:YES];
|
||||
}
|
||||
|
||||
uint64_t buttonIndex = 9999999;
|
||||
UIView* deepestButton;
|
||||
for (UIView *subview in self->_streamFrameTopLayerView.subviews) {
|
||||
if ([subview isKindOfClass:[OnScreenWidgetView class]]) {
|
||||
OnScreenWidgetView* widget = (OnScreenWidgetView* ) subview;
|
||||
if(widget.widgetType == WidgetTypeEnumButton){
|
||||
uint64_t index = [self->_streamFrameTopLayerView.subviews indexOfObject:subview];
|
||||
if (index<buttonIndex){
|
||||
buttonIndex = index;
|
||||
deepestButton = subview;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(deepestButton){
|
||||
for (UIView *subview in self->_streamFrameTopLayerView.subviews) {
|
||||
if ([subview isKindOfClass:[OnScreenWidgetView class]]) {
|
||||
OnScreenWidgetView* widget = (OnScreenWidgetView* ) subview;
|
||||
if(widget.widgetType == WidgetTypeEnumTouchPad){
|
||||
[self->_streamFrameTopLayerView insertSubview:subview belowSubview:deepestButton];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnScreenWidgetView.unfoldedExclusiveFolderSequence = oscProfile.unfoldedExclusiveFolderSequence;
|
||||
[OnScreenWidgetView setPostExclusiveUnfoldeds:oscProfile.postExclusiveUnfoldedSequences];
|
||||
[OnScreenWidgetView restoreFoldedStates];
|
||||
|
||||
NSLog(@"hasLegacyWidget %d %f", hasLegacyWidget, CACurrentMediaTime());
|
||||
// legacy widgets
|
||||
if(!OnScreenWidgetView.editMode && (hasLegacyWidget || !reload)){
|
||||
if([self isOscEnabled]) [self reloadLegacyWidgets:oscProfile];
|
||||
else [self disableOnScreenControls];
|
||||
}
|
||||
|
||||
if(reload && !OnScreenWidgetView.editMode){
|
||||
[PencilHandler.shared setupPressureLUTWithProfile:oscProfile];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (OnScreenControlsLevel) getCurrentOscState {
|
||||
if (onScreenControls == nil) {
|
||||
if (_onScreenControls == nil) {
|
||||
return OnScreenControlsLevelOff;
|
||||
}
|
||||
else {
|
||||
return [onScreenControls getLevel];
|
||||
return [_onScreenControls getLevel];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,6 +821,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
CGPoint location = [self adjustCoordinatesForVideoArea:[event locationInView:self]];
|
||||
CGSize videoSize = [self getVideoAreaSize];
|
||||
|
||||
// CGFloat pressure = (event.force / event.maximumPossibleForce) / sin(event.altitudeAngle);
|
||||
|
||||
return LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height,
|
||||
(event.force / event.maximumPossibleForce) / sin(event.altitudeAngle),
|
||||
0.0f, 0.0f,
|
||||
@@ -629,16 +837,27 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
case UIGestureRecognizerStateBegan:
|
||||
case UIGestureRecognizerStateChanged:
|
||||
type = LI_TOUCH_EVENT_HOVER;
|
||||
isPencilHovering = true;
|
||||
if(!PencilHandler.hoverSupported) PencilHandler.hoverSupported = true;
|
||||
// if(gesture.state == UIGestureRecognizerStateChanged) NSLog(@"UIGestureRecognizerStateChanged %f", CACurrentMediaTime());
|
||||
|
||||
break;
|
||||
|
||||
case UIGestureRecognizerStateEnded:
|
||||
type = LI_TOUCH_EVENT_HOVER_LEAVE;
|
||||
// NSLog(@"UIGestureRecognizerStateEnded %f", CACurrentMediaTime());
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if(gesture.state==UIGestureRecognizerStateEnded){
|
||||
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.008 * NSEC_PER_SEC));
|
||||
dispatch_after(delayTime, dispatch_get_main_queue(), ^{// Code to execute after the delay
|
||||
self->isPencilHovering = false;
|
||||
});
|
||||
}
|
||||
|
||||
CGPoint location = [self adjustCoordinatesForVideoArea:[gesture locationInView:self]];
|
||||
CGSize videoSize = [self getVideoAreaSize];
|
||||
|
||||
@@ -658,8 +877,26 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
}
|
||||
#endif
|
||||
|
||||
LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height,
|
||||
distance, 0.0f, 0.0f, rotationAngle, tiltAngle);
|
||||
|
||||
dispatch_after(0, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE,0), ^{// Code to execute after the delay
|
||||
if(PencilHandler.isDrawing) return;
|
||||
switch (PencilHandler.hoverMode) {
|
||||
case HoverPencil:
|
||||
LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height, distance, 0.0f, 0.0f, rotationAngle, tiltAngle);
|
||||
break;
|
||||
case HoverMouse:
|
||||
[self updateCursorLocation:location isMouse:YES];
|
||||
break;
|
||||
case HoverDisabled:
|
||||
break;
|
||||
case HoverBoth:
|
||||
LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height, distance, 0.0f, 0.0f, rotationAngle, tiltAngle);
|
||||
[self updateCursorLocation:location isMouse:YES];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -672,13 +909,18 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
[touchHandler touchesBegan:touches withEvent:event];
|
||||
return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only.
|
||||
}
|
||||
if (@available(iOS 13.4, *)) { // now only pencil events are restricted
|
||||
for (UITouch* touch in touches) {
|
||||
if (touch.type == UITouchTypePencil) {
|
||||
if ([self sendStylusEvent:touch]) return;
|
||||
}
|
||||
NSMutableSet* pencilTouches = [NSMutableSet set];
|
||||
for (UITouch* touch in touches) {
|
||||
if (touch.type == UITouchTypePencil) {
|
||||
[pencilTouches addObject:touch];
|
||||
}
|
||||
}
|
||||
NSMutableSet* nonPencilTouches;
|
||||
if(pencilTouches.count>0){
|
||||
[_pencilHandler touchesBegan:pencilTouches withEvent:event];
|
||||
nonPencilTouches = [touches mutableCopy];
|
||||
[nonPencilTouches minusSet:pencilTouches];
|
||||
}
|
||||
|
||||
#endif
|
||||
if ([self handleMouseButtonEvent:BUTTON_ACTION_PRESS
|
||||
@@ -693,22 +935,29 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
// Notify of user interaction and start expiration timer
|
||||
[self startInteractionTimer];
|
||||
|
||||
NSSet* targetTouches = nonPencilTouches ? nonPencilTouches : touches;
|
||||
if(settings.touchMode.intValue == NativeTouch || settings.touchMode.intValue == RelativeTouch){
|
||||
[self->onScreenControls handleTouchDownEvent:touches];
|
||||
[self->touchHandler touchesBegan:touches withEvent:event];
|
||||
[self->_onScreenControls handleTouchDownEvent:targetTouches];
|
||||
[self->touchHandler touchesBegan:targetTouches withEvent:event];
|
||||
}
|
||||
else if(![onScreenControls handleTouchDownEvent:touches]) [touchHandler touchesBegan:touches withEvent:event];
|
||||
else if(![_onScreenControls handleTouchDownEvent:targetTouches]) [touchHandler touchesBegan:targetTouches withEvent:event];
|
||||
}
|
||||
|
||||
- (UIBarButtonItem *)createButtonWithImageNamed:(NSString *)imageName backgroundColor:(UIColor *)backgroundColor target:(id)target action:(SEL)action keyCode:(NSInteger)keyCode isToggleable:(BOOL)isToggleable {
|
||||
- (UIBarButtonItem *)createButtonWithImageNamed:(NSString *)imageName backgroundColor:(UIColor *)backgroundColor target:(id)target action:(SEL)action keyCode:(NSInteger)keyCode isToggleable:(BOOL)isToggleable isDoneButton:(bool)isDoneButton {
|
||||
UIImage *image = [UIImage imageNamed:imageName];
|
||||
|
||||
|
||||
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
// [button setTitle:@"666" forState:UIControlStateNormal];
|
||||
[button setImage:image forState:UIControlStateNormal];
|
||||
button.frame = CGRectMake(0, 0, 30, 30);
|
||||
|
||||
button.frame = GenericUtils.liquidGlassEnabled ? CGRectMake(0, 0, 30, 30) : CGRectMake(0, 0, 30, 30);
|
||||
button.imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
button.imageView.backgroundColor = backgroundColor;
|
||||
button.imageView.layer.cornerRadius = 10.0;
|
||||
button.imageEdgeInsets = UIEdgeInsetsMake(6, 6, 6, 6);
|
||||
button.imageEdgeInsets = (GenericUtils.liquidGlassEnabled
|
||||
? (isDoneButton ? UIEdgeInsetsMake(16, 16, 16, 16) : UIEdgeInsetsMake(27.5, 27.5, 27.5, 27.5))
|
||||
: UIEdgeInsetsMake(6, 6, 6, 6));
|
||||
[button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
|
||||
objc_setAssociatedObject(button, "keyCode", @(keyCode), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
objc_setAssociatedObject(button, "isToggleable", @(isToggleable), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
@@ -724,7 +973,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
isOn = !isOn;
|
||||
// Update the button's appearance based on its new state
|
||||
if (isOn) {
|
||||
sender.imageView.backgroundColor = [UIColor lightGrayColor];
|
||||
sender.imageView.backgroundColor = GenericUtils.liquidGlassEnabled ? [UIColor.systemGrayColor colorWithAlphaComponent:0.5] : [UIColor lightGrayColor];
|
||||
} else {
|
||||
sender.imageView.backgroundColor = [UIColor blackColor];
|
||||
}
|
||||
@@ -760,8 +1009,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
|
||||
- (BOOL)handleMouseButtonEvent:(int)buttonAction forTouches:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
|
||||
|
||||
// NSLog(@"mouse click time: %f", CACurrentMediaTime()*1000);
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 13.4, *)) {
|
||||
UITouch* touch = [touches anyObject];
|
||||
@@ -830,9 +1077,10 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only.
|
||||
}
|
||||
|
||||
NSMutableSet* pencilTouches = [NSMutableSet set];
|
||||
for (UITouch* touch in touches) {
|
||||
if (touch.type == UITouchTypePencil) {
|
||||
if ([self sendStylusEvent:touch]) return;
|
||||
[pencilTouches addObject:touch];
|
||||
}
|
||||
if (@available(iOS 13.4, *)) {
|
||||
UITouch *touch = [touches anyObject];
|
||||
@@ -849,20 +1097,27 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
// don't require this, but we do it anyway for them too.
|
||||
// Cursor movement without a button held down is handled
|
||||
// in pointerInteraction:regionForRequest:defaultRegion.
|
||||
[self updateCursorLocation:[touch locationInView:self] isMouse:YES];
|
||||
if(!isPencilHovering) [self updateCursorLocation:[touch locationInView:self] isMouse:YES];
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
NSMutableSet* nonPencilTouches;
|
||||
if(pencilTouches.count>0){
|
||||
[_pencilHandler touchesMoved:pencilTouches withEvent:event];
|
||||
nonPencilTouches = [touches mutableCopy];
|
||||
[nonPencilTouches minusSet:pencilTouches];
|
||||
}
|
||||
|
||||
hasUserInteracted = YES;
|
||||
|
||||
NSSet* targetTouches = nonPencilTouches ? nonPencilTouches : touches;
|
||||
if(self->settings.touchMode.intValue == NativeTouch || self->settings.touchMode.intValue == RelativeTouch){
|
||||
[self->touchHandler touchesMoved:touches withEvent:event];
|
||||
[self->onScreenControls handleTouchMovedEvent:touches];
|
||||
[self->touchHandler touchesMoved:targetTouches withEvent:event];
|
||||
[self->_onScreenControls handleTouchMovedEvent:targetTouches];
|
||||
}
|
||||
else if(![self->onScreenControls handleTouchMovedEvent:touches]) [self->touchHandler touchesMoved:touches withEvent:event];
|
||||
else if(![self->_onScreenControls handleTouchMovedEvent:targetTouches]) [self->touchHandler touchesMoved:targetTouches withEvent:event];
|
||||
}
|
||||
|
||||
- (void) handleKeyCombos:(UIPress*) press{
|
||||
@@ -871,16 +1126,19 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
}
|
||||
switch (press.key.keyCode) {
|
||||
case UIKeyboardHIDUsageKeyboardQ:
|
||||
[interactionDelegate streamExitRequested];
|
||||
[_interactionDelegate streamExitRequested];
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardS:
|
||||
[interactionDelegate toggleStatsOverlay];
|
||||
[_interactionDelegate toggleStatsOverlay];
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardM:
|
||||
[interactionDelegate toggleMouseCapture];
|
||||
[_interactionDelegate toggleMouseCapture];
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardC:
|
||||
[interactionDelegate toggleMouseVisible];
|
||||
[_interactionDelegate toggleMouseVisible];
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardD:
|
||||
[_interactionDelegate disconnectAndQuitApp];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -934,17 +1192,22 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only.
|
||||
}
|
||||
|
||||
if (@available(iOS 13.4, *)){ //now only pencil events are restricted
|
||||
for (UITouch* touch in touches) {
|
||||
if (touch.type == UITouchTypePencil) {
|
||||
if ([self sendStylusEvent:touch]) return;
|
||||
}
|
||||
NSMutableSet* pencilTouches = [NSMutableSet set];
|
||||
for (UITouch* touch in touches) {
|
||||
if (touch.type == UITouchTypePencil) {
|
||||
[pencilTouches addObject:touch];
|
||||
}
|
||||
}
|
||||
NSMutableSet* nonPencilTouches;
|
||||
if(pencilTouches.count>0){
|
||||
[_pencilHandler touchesEnded:pencilTouches withEvent:event];
|
||||
nonPencilTouches = [touches mutableCopy];
|
||||
[nonPencilTouches minusSet:pencilTouches];
|
||||
}
|
||||
|
||||
#endif
|
||||
if ([self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
|
||||
forTouches:touches
|
||||
forTouches:nonPencilTouches ? nonPencilTouches : touches
|
||||
withEvent:event]) {
|
||||
// If it's a mouse event, we're done
|
||||
return;
|
||||
@@ -954,22 +1217,21 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
|
||||
hasUserInteracted = YES;
|
||||
|
||||
NSSet* targetTouches = nonPencilTouches ? nonPencilTouches : touches;
|
||||
if(settings.touchMode.intValue == NativeTouch || settings.touchMode.intValue == RelativeTouch){
|
||||
[self->touchHandler touchesEnded:touches withEvent:event]; // when touches ended, must call the native touchhandler before onScreenControls, since the NSSet of touches captured by on screen button shall be updated later
|
||||
[self->onScreenControls handleTouchUpEvent:touches];
|
||||
[self->touchHandler touchesEnded:targetTouches withEvent:event]; // when touches ended, must call the native touchhandler before onScreenControls, since the NSSet of touches captured by on screen button shall be updated later
|
||||
[self->_onScreenControls handleTouchUpEvent:targetTouches];
|
||||
}
|
||||
else if(![onScreenControls handleTouchUpEvent:touches]) [touchHandler touchesEnded:touches withEvent:event];
|
||||
else if(![_onScreenControls handleTouchUpEvent:targetTouches]) [touchHandler touchesEnded:targetTouches withEvent:event];
|
||||
}
|
||||
|
||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
[touchHandler touchesCancelled:touches withEvent:event];
|
||||
#if !TARGET_OS_TV
|
||||
if (settings.touchMode.intValue == NativeTouchOnly) return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only.
|
||||
if (@available(iOS 13.4, *)){ //now only pencil events are restricted
|
||||
for (UITouch* touch in touches) {
|
||||
if (touch.type == UITouchTypePencil) {
|
||||
if ([self sendStylusEvent:touch]) return;
|
||||
}
|
||||
for (UITouch* touch in touches) {
|
||||
if (touch.type == UITouchTypePencil) {
|
||||
[self touchesEnded:touches withEvent:event];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1026,7 +1288,10 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
// Move the cursor on the host if no buttons are pressed.
|
||||
// Motion with buttons pressed in handled in touchesMoved:
|
||||
if (lastMouseButtonMask == 0) {
|
||||
[self updateCursorLocation:request.location isMouse:YES];
|
||||
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.001 * NSEC_PER_SEC));
|
||||
dispatch_after(delayTime, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE,0), ^{
|
||||
if(!self->isPencilHovering) [self updateCursorLocation:request.location isMouse:YES];
|
||||
});
|
||||
}
|
||||
|
||||
// The pointer interaction should cover the video region only
|
||||
@@ -1140,6 +1405,9 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
}
|
||||
|
||||
- (void)onKeyboardPressed:(UITextField *)textField {
|
||||
|
||||
if (textField.markedTextRange) return;
|
||||
|
||||
NSString* inputText = textField.text;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
// If the text became empty, we know the user pressed the backspace key.
|
||||
@@ -1302,6 +1570,42 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
LiSendScrollEvent(deltaZ);
|
||||
}
|
||||
|
||||
- (void)alterAbsTouchDragWith:(int32_t)mouseButton{
|
||||
if([touchHandler isKindOfClass:[AbsoluteTouchHandler class]]){
|
||||
AbsoluteTouchHandler* handler = (AbsoluteTouchHandler* )touchHandler;
|
||||
AbsoluteTouchHandler.mouseButtonForCursorMove = mouseButton;
|
||||
[handler pauseLeftButtonDrag];
|
||||
}
|
||||
else return;
|
||||
}
|
||||
|
||||
- (void)switchPencilHover{
|
||||
[_pencilHandler switchPencilHover];
|
||||
}
|
||||
|
||||
- (void)enablePencilHover{
|
||||
[_pencilHandler enablePencilHover];
|
||||
}
|
||||
|
||||
- (void)disablePencilHover{
|
||||
[_pencilHandler disablePencilHover];
|
||||
}
|
||||
|
||||
- (void)setAllowSingleTouchEnabled:(BOOL)enabled{
|
||||
if([touchHandler isKindOfClass:[NativeTouchHandler class]]){
|
||||
NativeTouchHandler* handler = (NativeTouchHandler* )touchHandler;
|
||||
[handler setAllowSingleTouchEnabled:enabled];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cleanUp{
|
||||
[keyInputField resignFirstResponder];
|
||||
keyInputField.delegate = nil;
|
||||
}
|
||||
|
||||
- (void)dealloc{
|
||||
NSLog(@"dealloc streamView %f", CACurrentMediaTime());
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (BOOL)isMultipleTouchEnabled {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// TouchPadGestureHandler.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025/11/5.
|
||||
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc class TouchPadGestureHandler: NSObject {
|
||||
|
||||
@objc public static var ctrlDown:Bool = false
|
||||
@objc public static var enablePinch:Bool = true
|
||||
@objc public static var ctrlDownForPinch:Bool = true
|
||||
@objc public static var enableHorizontalScroll:Bool = true
|
||||
@objc public static var scrollSensitivity:CGFloat = 1.0
|
||||
@objc public static var pinchSensitivity:CGFloat = 1.0
|
||||
@objc public static var displayLinkRate:CGFloat = 60
|
||||
|
||||
private static var inertialScroller: InertialScroller = InertialScroller(decelerationRate: displayLinkRate > 110 ? 0.96 : 0.9, displayLinkRate: displayLinkRate) {
|
||||
if ctrlDown {return}
|
||||
LiSendHighResScrollEvent(Int16(inertialScroller.vector.dy*7*scrollSensitivity))
|
||||
if TouchPadGestureHandler.enableHorizontalScroll {LiSendHighResHScrollEvent(Int16(-inertialScroller.vector.dx*7*scrollSensitivity))}
|
||||
}
|
||||
|
||||
@objc public static func startInertialScroll(){
|
||||
inertialScroller.timer?.restart()
|
||||
}
|
||||
|
||||
@objc public static func handleGesture(in view: UIView, with event: UIEvent) {
|
||||
inertialScroller.timer?.pause()
|
||||
|
||||
let currentTouches = UITouchUtil.touches(in: view, from: event)
|
||||
guard currentTouches.count == 2 else { return }
|
||||
|
||||
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_LEFT)
|
||||
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_RIGHT)
|
||||
|
||||
guard let touch1 = currentTouches.first else { return }
|
||||
var mutable = Array(currentTouches)
|
||||
mutable.removeAll { $0 == touch1 }
|
||||
guard let touch2 = mutable.first else { return }
|
||||
|
||||
let currentDistance = UITouchUtil.distance(between: touch1, and: touch2, in: view)
|
||||
let previousDistance = UITouchUtil.previousDistance(between: touch1, and: touch2, in: view)
|
||||
|
||||
let midPointVector = UITouchUtil.midPointVector(between: touch1, and: touch2, in: view)
|
||||
let midPointDeltaX = midPointVector.dx
|
||||
let midPointDeltaY = midPointVector.dy
|
||||
|
||||
inertialScroller.vector = CGVector(dx: midPointDeltaX, dy: midPointDeltaY)
|
||||
|
||||
let originalPinchDelta = currentDistance-previousDistance;
|
||||
let pinchDelta = enablePinch ? originalPinchDelta*7*pinchSensitivity : 0;
|
||||
LiSendHighResScrollEvent(Int16(pinchDelta + midPointDeltaY*7*scrollSensitivity))
|
||||
if enableHorizontalScroll {LiSendHighResHScrollEvent(Int16(-midPointDeltaX*7*scrollSensitivity))}
|
||||
|
||||
if enablePinch, ctrlDownForPinch {
|
||||
let midPointDelta = hypot(midPointDeltaX, midPointDeltaY)
|
||||
if abs(originalPinchDelta) > midPointDelta*1.3, midPointDelta < 2 {
|
||||
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["CTRL"]!, CChar(KEY_ACTION_DOWN), 0)
|
||||
ctrlDown = true
|
||||
} else {
|
||||
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["CTRL"]!, CChar(KEY_ACTION_UP), 0)
|
||||
ctrlDown = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// UITouchUtil.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025/11/5.
|
||||
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc class UITouchUtil: NSObject {
|
||||
@objc public static func touches(in view: UIView, from event: UIEvent?) -> Set<UITouch> {
|
||||
guard (event?.allTouches) != nil else { return [] }
|
||||
return event?.allTouches?.filter({ $0.view == view && $0.type == .direct}) ?? []
|
||||
}
|
||||
|
||||
@objc public static func distance(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGFloat {
|
||||
guard let view = view, let touch1 = touch1, let touch2 = touch2 else { return 0 }
|
||||
let p1 = touch1.location(in: view)
|
||||
let p2 = touch2.location(in: view)
|
||||
return hypot(p1.x - p2.x, p1.y - p2.y)
|
||||
}
|
||||
|
||||
@objc public static func previousDistance(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGFloat {
|
||||
guard let view = view, let touch1 = touch1, let touch2 = touch2 else { return 0 }
|
||||
let p1 = touch1.previousLocation(in: view)
|
||||
let p2 = touch2.previousLocation(in: view)
|
||||
return hypot(p1.x - p2.x, p1.y - p2.y)
|
||||
}
|
||||
|
||||
@objc public static func vector(of touch: UITouch?, in view: UIView?) -> CGVector {
|
||||
guard let view = view, let touch = touch else { return CGVectorMake(0,0) }
|
||||
let p2 = touch.location(in: view)
|
||||
let p1 = touch.previousLocation(in: view)
|
||||
return CGVectorMake(p2.x - p1.x, p2.y - p1.y)
|
||||
}
|
||||
|
||||
@objc public static func midPointVector(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGVector {
|
||||
guard let view = view, let touch1 = touch1, let touch2 = touch2 else { return CGVector(dx: 0, dy: 0)}
|
||||
|
||||
let vector1 = vector(of: touch1, in: view)
|
||||
let vector2 = vector(of: touch2, in: view)
|
||||
|
||||
let deltaX = (vector1.dx+vector2.dx)/2
|
||||
let deltaY = (vector1.dy+vector2.dy)/2
|
||||
|
||||
return CGVector(dx: deltaX, dy: deltaY)
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>GCSupportsGameMode</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDuration</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
@@ -63,22 +61,19 @@
|
||||
<string>_nvstream._tcp</string>
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
|
||||
<string>VoidLink uses the local network to connect to your gaming PC for streaming.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>VoidLink需要麦克风访问权限。现阶段,麦克风重定向仅支持Sunshine基地版,且麦克风音频流【未经加密】。
|
||||
|
||||
VoidLink uses microphone to stream your voice to gaming PC. Microphone redirection currently works with Sunshine-Foundation (a fork of Sunshine) only, and the mic audio stream is NOT ENCRYPTED at this stage.</string>
|
||||
<key>NSMotionUsageDescription</key>
|
||||
<string>Moonlight uses the device Gyro and Acceleration data to emulate motion controls when streaming.</string>
|
||||
<string>VoidLink uses the device Gyro and Acceleration data to emulate motion controls when streaming.</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleExternalDisplay</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>SceneDelegate</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>External Display Configuration</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -88,9 +83,16 @@
|
||||
<string>SceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIWindowSceneSessionRoleExternalDisplay</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>External Display Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>SceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
@@ -98,6 +100,8 @@
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UIDesignRequiresCompatibility</key>
|
||||
<false/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>Launch Screen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788.4" systemVersion="24F74" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24512" systemVersion="24G419" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
@@ -8,6 +8,7 @@
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="activeAddressPool" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
@@ -21,52 +22,92 @@
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="appTheme" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="asyncFrameDequeue" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="asyncNativeTouchPriority" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="audioEngine" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="backroundSessionTimer" optional="YES" attributeType="Integer 32" defaultValueString="32767" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="emulatedControllerType" optional="YES" attributeType="Integer 16" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="buttonVisualFeedback" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="controllerMouseExpo" optional="YES" attributeType="Float" defaultValueString="1.6" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="controllerMouseLeftButton" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="controllerMousePointerVelocity" optional="YES" attributeType="Float" defaultValueString="12" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="controllerMouseRightButton" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="controllerMouseStick" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="controllerMouseSwitch" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="ctrlDownForPinch" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="delayLeftClick" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="deviceGyroMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="duckOtherApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="edgeSlidingSensitivity" optional="YES" attributeType="Float" defaultValueString="22" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="emulatedControllerType" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="enableFrameTimebase" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enableGraphs" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enablePinch" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enablePIP" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enableYUV444" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="externalDisplayMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="frameQueueSize" optional="YES" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="framePacingMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="framePacingMode" optional="YES" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="frameQueueSize" optional="YES" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="fullColorRange" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="graphOpacity" optional="YES" attributeType="Integer 32" defaultValueString="50" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="gyroBiasX" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="gyroBiasY" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="gyroBiasZ" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="gyroMode" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="gyroSensitivity" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="graphOpacity" optional="YES" attributeType="Integer 32" defaultValueString="50" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="hapticEngine" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="keyboardToggleFingers" optional="YES" attributeType="Integer 16" defaultValueString="3" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="leftClickDelayMs" optional="YES" attributeType="Float" defaultValueString="15" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="liftStreamViewForKeyboard" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="localMousePointerMode" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="localVolume" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="mapControllerToMouse" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="micVolume" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="mousePointerVelocityFactor" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="muteInBackground" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="oscLayoutToolFingers" optional="YES" attributeType="Integer 16" defaultValueString="4" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="passthroughGestures" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="pencilTickIntervalUs" optional="YES" attributeType="Float" defaultValueString="2000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="pencilTickMode" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="pinchSensitivity" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="pointerVelocityModeDivider" optional="YES" attributeType="Float" defaultValueString="0.5" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="preferredCodec" attributeType="Integer 32" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="preferredCodec" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="redirectMic" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="relativeTouchSlideThreshold" optional="YES" attributeType="Float" defaultValueString="6" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="rememberFoldState" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="renderingBackend" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="resolutionSelected" optional="YES" attributeType="Integer 16" defaultValueString="4" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="reverseMouseWheelDirection" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="scrollSensitivity" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="sdrPerformanceWorkaround" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="sendDummyEvent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="settingsMenuMode" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="settingsMenuOffset" optional="YES" attributeType="Float" defaultValueString="22" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="settingsMenuWidth" attributeType="Float" defaultValueString="370" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="showKeyboardToolbar" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="showKeyboardToolbar" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="singleTapSensitivity" optional="YES" attributeType="Double" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="slideToSettingsDistance" optional="YES" attributeType="Float" defaultValueString="0.2" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="slideToSettingsScreenEdge" optional="YES" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="statsOverlayEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="statsOverlayLevel" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="renderingBackend" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="touchMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="touchMoveEventInterval" optional="YES" attributeType="Integer 16" defaultValueString="320" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="touchMoveEventInterval" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="touchPointerVelocityFactor" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="touchPointTracking" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="unlockDisplayOrientation" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="useBuiltinMic" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
</model>
|
||||
@@ -19,25 +19,36 @@
|
||||
|
||||
@interface MenuSectionView : UIView
|
||||
|
||||
@property (class, nonatomic, assign) BOOL overridePersistedFoldState;
|
||||
|
||||
// 外部可访问属性
|
||||
@property (nonatomic, strong) UIStackView *rootStackView;
|
||||
@property (nonatomic, assign) CGFloat leadingTrailingPadding;
|
||||
@property (nonatomic, assign) CGFloat separatorLinePadding;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, copy) NSString *sectionTitle;
|
||||
@property (nonatomic, assign) BOOL expanded;
|
||||
@property (nonatomic, copy) NSString *identifier;
|
||||
@property (nonatomic, assign) BOOL isExpanded;
|
||||
@property (nonatomic, assign) BOOL expandable;
|
||||
@property (nonatomic, copy) void (^lockedSectionHandler)(void);
|
||||
@property (nonatomic, strong) UIColor *backgroundColor;
|
||||
@property (nonatomic, assign) CGFloat rootStackViewSpacing;
|
||||
@property (nonatomic, assign) CGFloat headerViewHeight;
|
||||
@property (nonatomic, assign) CGFloat headerViewVerticalSpacing;
|
||||
@property (nonatomic, strong) NSMutableArray<UIStackView *> *subStackViews;
|
||||
@property (nonatomic, strong) UIImageView *iconImageView;
|
||||
@property (nonatomic, strong) UIView *separatorLine;
|
||||
@property (nonatomic, weak) id<MenuSectionDelegate> delegate; // Delegate property
|
||||
|
||||
// 方法
|
||||
- (void)setSectionWithIcon:(UIImage *)icon andSize:(CGFloat)size;
|
||||
- (void)setSectionWithIcon:(UIImage *)icon size:(CGFloat)size sizeConstraint:(CGFloat)constant;
|
||||
- (void)setSectionWithIcon:(UIImage *)icon size:(CGFloat)size weight:(UIImageSymbolWeight)weight sizeConstraint:(CGFloat)constant API_AVAILABLE(ios(13.0));
|
||||
- (void)addSubStackView:(UIStackView *)stackView;
|
||||
- (void)addToParentStack:(UIStackView *)parentStack;
|
||||
- (void)removeSubStackView:(UIStackView *)stackView;
|
||||
- (void)updateLayout;
|
||||
- (void)updateViewForFoldState;
|
||||
- (void)setExpanded:(BOOL)overridePersistedState;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,24 +8,32 @@
|
||||
// MenuSectionView.m
|
||||
|
||||
#import "MenuSectionView.h"
|
||||
#import "ThemeManager.h"
|
||||
#import "VoidLink-Swift.h"
|
||||
|
||||
|
||||
@interface MenuSectionView ()
|
||||
|
||||
@property (nonatomic, strong) UIStackView *rootStackView;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UIImageView *iconImageView;
|
||||
@property (nonatomic, strong) UIButton *toggleButton;
|
||||
@property (nonatomic, strong) UIButton *toggleArea;
|
||||
@property (nonatomic, strong) NSLayoutConstraint *heightConstraint;
|
||||
@property (nonatomic, strong) UIView *headerView;
|
||||
@property (nonatomic, strong) UIView *separatorLine;
|
||||
@property (assign) CGFloat titleOffset;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MenuSectionView
|
||||
|
||||
static BOOL overridePersistedFoldState = YES;
|
||||
|
||||
+ (BOOL)overridePersistedFoldState {
|
||||
return overridePersistedFoldState;
|
||||
}
|
||||
|
||||
+ (void)setOverridePersistedFoldState:(BOOL)val {
|
||||
overridePersistedFoldState = val;
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
@@ -44,15 +52,17 @@
|
||||
|
||||
- (void)commonInit {
|
||||
// 默认值
|
||||
_leadingTrailingPadding = 0;
|
||||
_leadingTrailingPadding = 5;
|
||||
_separatorLinePadding = 40;
|
||||
_sectionTitle = @"Section";
|
||||
_expanded = YES;
|
||||
_isExpanded = YES;
|
||||
_expandable = true;
|
||||
_backgroundColor = [UIColor clearColor];
|
||||
_rootStackViewSpacing = [self isIPhone] ? 10 : 18;
|
||||
_rootStackViewSpacing = [self isIPhone] ? 10 : 12;
|
||||
_subStackViews = [NSMutableArray array];
|
||||
_headerViewHeight = 37;
|
||||
_headerViewVerticalSpacing = 25;
|
||||
_titleOffset = 41.5;
|
||||
|
||||
// 设置视图
|
||||
self.layer.cornerRadius = 10.0;
|
||||
@@ -68,14 +78,15 @@
|
||||
_iconImageView = [[UIImageView alloc] init];
|
||||
_iconImageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
_iconImageView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
_iconImageView.tintColor = [ThemeManager textColor];
|
||||
_iconImageView.tintColor = ThemeManager.textColor;
|
||||
[_headerView addSubview:_iconImageView];
|
||||
|
||||
// 标题标签
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
_titleLabel.text = _sectionTitle;
|
||||
_titleLabel.font = [UIFont systemFontOfSize:23 weight:UIFontWeightMedium];
|
||||
_titleLabel.textColor = [ThemeManager textColor];
|
||||
_titleLabel.accessibilityIdentifier = @"menuSectionTitleLabel";
|
||||
_titleLabel.font = [UIFont systemFontOfSize:19.5 weight:UIFontWeightMedium];
|
||||
_titleLabel.textColor = ThemeManager.textColor;
|
||||
_titleLabel.textAlignment = NSTextAlignmentLeft;
|
||||
_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[_headerView addSubview:_titleLabel];
|
||||
@@ -85,7 +96,7 @@
|
||||
_toggleArea = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:_headerViewHeight*0.31 weight:UIImageSymbolWeightBold];
|
||||
[_toggleButton setImage:[UIImage systemImageNamed:@"chevron.left" withConfiguration:config] forState:UIControlStateNormal];
|
||||
[_toggleButton setImage:[UIImage systemImageNamed:@"chevron.right" withConfiguration:config] forState:UIControlStateNormal];
|
||||
} else {
|
||||
// [_toggleButton setTitle:@"<" forState:UIControlStateNormal];
|
||||
}
|
||||
@@ -107,7 +118,7 @@
|
||||
|
||||
// 分隔线
|
||||
_separatorLine = [[UIView alloc] init];
|
||||
_separatorLine.backgroundColor = [ThemeManager separatorColor];
|
||||
_separatorLine.backgroundColor = ThemeManager.separatorColor;
|
||||
_separatorLine.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[self addSubview:_separatorLine];
|
||||
@@ -119,6 +130,10 @@
|
||||
// 布局
|
||||
[self setupConstraints];
|
||||
[self updateViewForFoldState];
|
||||
|
||||
self.lockedSectionHandler = ^{
|
||||
NSLog(@"Null execution of lockedSectionHandler");
|
||||
};
|
||||
}
|
||||
|
||||
- (void)setupConstraints {
|
||||
@@ -130,19 +145,10 @@
|
||||
[_headerView.heightAnchor constraintEqualToConstant:_headerViewHeight]
|
||||
]];
|
||||
|
||||
// 图标约束
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_iconImageView.centerXAnchor constraintEqualToAnchor:_headerView.leadingAnchor constant:_leadingTrailingPadding+_headerViewHeight/2-3],
|
||||
[_iconImageView.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
|
||||
// [_iconImageView.topAnchor constraintEqualToAnchor:_headerView.topAnchor],
|
||||
[_iconImageView.widthAnchor constraintEqualToConstant:_headerViewHeight-6],
|
||||
//[_iconImageView.heightAnchor constraintEqualToConstant:_headerViewHeight-6]
|
||||
]];
|
||||
|
||||
// 标题约束
|
||||
if(@available(iOS 13.0, *)){
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_titleLabel.leadingAnchor constraintEqualToAnchor:_iconImageView.trailingAnchor constant:8],
|
||||
[_titleLabel.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_titleOffset],
|
||||
[_titleLabel.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
|
||||
[_titleLabel.trailingAnchor constraintEqualToAnchor:_toggleButton.leadingAnchor constant:-8]
|
||||
]];
|
||||
@@ -166,7 +172,7 @@
|
||||
[_toggleArea.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
|
||||
[_toggleArea.trailingAnchor constraintEqualToAnchor:_toggleButton.leadingAnchor],
|
||||
//[_toggleArea.widthAnchor constraintEqualToConstant:_headerViewHeight],
|
||||
[_toggleArea.heightAnchor constraintEqualToConstant:_headerViewHeight]
|
||||
[_toggleArea.heightAnchor constraintEqualToConstant:_headerViewHeight+25]
|
||||
]];
|
||||
|
||||
[self layoutIfNeeded];
|
||||
@@ -185,9 +191,9 @@
|
||||
//[_separatorLine.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
|
||||
//[_separatorLine.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
|
||||
[_separatorLine.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
|
||||
[_separatorLine.widthAnchor constraintEqualToConstant:1000], // //mark: settingMenuLayout
|
||||
[_separatorLine.widthAnchor constraintEqualToAnchor:self.widthAnchor constant:-5], // //mark: settingMenuLayout
|
||||
[_separatorLine.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
|
||||
[_separatorLine.heightAnchor constraintEqualToConstant:2]
|
||||
[_separatorLine.heightAnchor constraintEqualToConstant:GenericUtils.menuSectionSeparatorWidth]
|
||||
]];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
@@ -198,7 +204,7 @@
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
- (void)setSectionWithIcon:(UIImage *)icon andSize:(CGFloat)size{
|
||||
- (void)setSectionWithIcon:(UIImage *)icon size:(CGFloat)size sizeConstraint:(CGFloat)constant{
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config;
|
||||
if(icon.isSymbolImage) config = [UIImageSymbolConfiguration configurationWithPointSize:size weight:UIImageSymbolWeightBold];
|
||||
@@ -210,8 +216,44 @@
|
||||
} else {
|
||||
_iconImageView.image = icon;
|
||||
}
|
||||
_iconImageView.tintColor = [ThemeManager textColor];
|
||||
_iconImageView.tintColor = ThemeManager.textColor;
|
||||
_iconImageView.hidden = (icon == nil);
|
||||
|
||||
// 图标约束
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_iconImageView.centerXAnchor constraintEqualToAnchor:_headerView.leadingAnchor constant:_leadingTrailingPadding+_headerViewHeight/2-3],
|
||||
[_iconImageView.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor constant:-0],
|
||||
[_iconImageView.widthAnchor constraintEqualToConstant:_headerViewHeight+constant],
|
||||
[_iconImageView.heightAnchor constraintEqualToConstant:_headerViewHeight+constant]
|
||||
]];
|
||||
|
||||
[self updateLayout];
|
||||
}
|
||||
|
||||
- (void)setSectionWithIcon:(UIImage *)icon size:(CGFloat)size weight:(UIImageSymbolWeight)weight sizeConstraint:(CGFloat)constant API_AVAILABLE(ios(13.0)){
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config;
|
||||
if(icon.isSymbolImage) config = [UIImageSymbolConfiguration configurationWithPointSize:size weight:weight];
|
||||
else{
|
||||
config = [UIImageSymbolConfiguration configurationWithPointSize:size];
|
||||
icon = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
}
|
||||
_iconImageView.image = [icon imageWithConfiguration:config];
|
||||
} else {
|
||||
_iconImageView.image = icon;
|
||||
}
|
||||
_iconImageView.tintColor = ThemeManager.textColor;
|
||||
_iconImageView.hidden = (icon == nil);
|
||||
|
||||
// 图标约束
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[_iconImageView.centerXAnchor constraintEqualToAnchor:_headerView.leadingAnchor constant:_leadingTrailingPadding+_headerViewHeight/2-3],
|
||||
[_iconImageView.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
|
||||
// [_iconImageView.topAnchor constraintEqualToAnchor:_headerView.topAnchor],
|
||||
[_iconImageView.widthAnchor constraintEqualToConstant:_headerViewHeight+constant],
|
||||
[_iconImageView.heightAnchor constraintEqualToConstant:_headerViewHeight+constant]
|
||||
]];
|
||||
|
||||
[self updateLayout];
|
||||
}
|
||||
|
||||
@@ -226,9 +268,10 @@
|
||||
}
|
||||
|
||||
- (void)setExpanded:(BOOL)isExpanded {
|
||||
if (_expanded != isExpanded) {
|
||||
_expanded = isExpanded;}
|
||||
[self updateViewForFoldState];
|
||||
self.isExpanded = !_expandable ? false : isExpanded;
|
||||
[self updateViewForFoldState];
|
||||
[[NSUserDefaults standardUserDefaults] setBool:self.isExpanded forKey:self.identifier];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
@@ -264,6 +307,11 @@
|
||||
[self.leadingAnchor constraintEqualToAnchor:parentStack.leadingAnchor constant:0],
|
||||
[self.trailingAnchor constraintEqualToAnchor:parentStack.trailingAnchor constant:0],
|
||||
]];
|
||||
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
BOOL persistedFoldState = [defaults objectForKey:self.identifier] ? [defaults boolForKey:self.identifier] : YES;
|
||||
|
||||
[self setExpanded: MenuSectionView.overridePersistedFoldState ? _expandable : persistedFoldState];
|
||||
}
|
||||
|
||||
- (void)removeSubStackView:(UIStackView *)stackView {
|
||||
@@ -308,8 +356,11 @@
|
||||
}
|
||||
|
||||
- (void)toggleFold {
|
||||
self.expanded = !self.expanded;
|
||||
self.isExpanded = !_expandable ? false : !self.isExpanded;
|
||||
if(!_expandable && self.lockedSectionHandler) self.lockedSectionHandler();
|
||||
[self updateViewForFoldState];
|
||||
[[NSUserDefaults standardUserDefaults] setBool:self.isExpanded forKey:self.identifier];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
- (SettingsMenuMode)getSettingsMenuMode {
|
||||
@@ -320,7 +371,9 @@
|
||||
|
||||
|
||||
- (void)updateViewForFoldState {
|
||||
[self setupConstraints];
|
||||
// [self setupConstraints];
|
||||
// don't run this repeatedly
|
||||
|
||||
NSInteger visibleCount = 0;
|
||||
for (UIView *subview in _rootStackView.arrangedSubviews) {
|
||||
if (!subview.isHidden) {
|
||||
@@ -328,7 +381,7 @@
|
||||
}
|
||||
}
|
||||
self.hidden = visibleCount == 0;
|
||||
if (_expanded) {
|
||||
if (_isExpanded) {
|
||||
_rootStackView.hidden = NO;
|
||||
_separatorLine.hidden = NO;
|
||||
CGSize fittingSize = [self.rootStackView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
|
||||
@@ -340,7 +393,7 @@
|
||||
self.heightConstraint.constant = headerHeight + _headerViewVerticalSpacing + rootStackViewHeight + _separatorLinePadding + 1;
|
||||
|
||||
[UIView animateWithDuration:0 animations:^{
|
||||
self.toggleButton.transform = CGAffineTransformMakeRotation(M_PI+M_PI_2);
|
||||
self.toggleButton.transform = CGAffineTransformMakeRotation(M_PI/2);
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.rootStackView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-self.separatorLinePadding],
|
||||
// [self.rootStackView.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor constant:0],
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#import "ConnectionCallbacks.h"
|
||||
#import "Frame.h"
|
||||
#import "Plot.h"
|
||||
#import "TemporarySettings.h"
|
||||
|
||||
@interface MetalVideoRenderer : NSObject
|
||||
|
||||
@@ -22,11 +23,13 @@
|
||||
@property (nonatomic) id<CAMetalDrawable> _Nullable nextDrawable;
|
||||
@property (atomic) BOOL isStopping;
|
||||
@property (nonatomic) BOOL hdrEnabled;
|
||||
@property (nonatomic, readonly, nonnull) dispatch_semaphore_t inFlightSemaphore;
|
||||
|
||||
- (nonnull instancetype)initWithMetalDevice:(nonnull id<MTLDevice>)device drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat framerate:(float)framerate hdrEnabled:(BOOL)hdrEnabled;
|
||||
- (void)renderFrame:(nonnull Frame *)frame withDrawable:(nonnull id<CAMetalDrawable>)drawable API_AVAILABLE(ios(17.0));
|
||||
- (void)renderFrame:(nonnull Frame *)frame withDrawable:(nonnull id<CAMetalDrawable>)drawable targetPresentationTimestamp:(CFTimeInterval)targetPresentationTimestamp API_AVAILABLE(ios(17.0));
|
||||
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
|
||||
- (instancetype _Nonnull )initWithMetalDevice:(id<MTLDevice>_Nonnull)device drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat settings:(TemporarySettings* _Nonnull )currentSettings;
|
||||
|
||||
|
||||
- (void)renderFrame:(nonnull Frame *)frame toLayer:(nonnull CAMetalLayer *)layer;
|
||||
- (BOOL)waitToRenderTo:(nonnull CAMetalLayer *)layer API_AVAILABLE(ios(13.0));
|
||||
- (void)drawableResize:(CGSize)drawableSize;
|
||||
- (void)shutdown;
|
||||
|
||||
|
||||
@@ -131,24 +131,35 @@ CFStringRef __currentColorSpace;
|
||||
id<MTLBuffer> _CscParamsBuffer;
|
||||
id<MTLBuffer> _VideoVertexBuffer;
|
||||
|
||||
CFStringRef _nonFullHdrColorSpace;
|
||||
MTLPixelFormat _nonFullHdrPixelFormat;
|
||||
|
||||
// https://developer.apple.com/documentation/metal/synchronizing-cpu-and-gpu-work?language=objc
|
||||
dispatch_semaphore_t _inFlightSemaphore;
|
||||
}
|
||||
|
||||
- (instancetype)initWithMetalDevice:(id<MTLDevice>)device drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat framerate:(float)framerate hdrEnabled:(BOOL)hdrEnabled {
|
||||
@synthesize inFlightSemaphore = _inFlightSemaphore;
|
||||
|
||||
- (instancetype)initWithMetalDevice:(id<MTLDevice>)device drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat settings:(TemporarySettings* )currentSettings {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_sq = dispatch_queue_create("com.moonlight.MetalVideoRenderer",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
_averageGPUTime = (1.0f / framerate) / 2;
|
||||
_averageGPUTime = (1.0f / currentSettings.framerate.floatValue) / 2;
|
||||
_device = device;
|
||||
_colorPixelFormat = MTLPixelFormatBGR10A2Unorm;
|
||||
_framerate = framerate;
|
||||
_hdrEnabled = hdrEnabled;
|
||||
_colorPixelFormat = drawablePixelFormat;
|
||||
_framerate = currentSettings.framerate.floatValue;
|
||||
_hdrEnabled = currentSettings.enableHdr;
|
||||
_commandQueue = [_device newCommandQueue];
|
||||
_currentEDRHeadroom = 1.0f;
|
||||
_lastColorSpace = -1;
|
||||
_lastFullRange = NO;
|
||||
|
||||
// Initialize default CSC parameters buffer with BT.601 limited range
|
||||
struct ParamBuffer defaultParamBuffer;
|
||||
defaultParamBuffer.cscParams = k_CscParams_Bt601Lim;
|
||||
MTLResourceOptions bufferOptions = MTLResourceStorageModeShared;
|
||||
_CscParamsBuffer = [_device newBufferWithBytes:(void *)&defaultParamBuffer length:sizeof(defaultParamBuffer) options:bufferOptions];
|
||||
_lastPresented = 0.0f;
|
||||
_inFlightSemaphore = dispatch_semaphore_create(MaxFramesInFlight);
|
||||
_isStopping = NO;
|
||||
@@ -164,6 +175,10 @@ CFStringRef __currentColorSpace;
|
||||
_renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0);
|
||||
_renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
|
||||
|
||||
if (@available(iOS 14.0, *)) _nonFullHdrColorSpace = kCGColorSpaceITUR_2100_PQ;
|
||||
else _nonFullHdrColorSpace = kCGColorSpaceITUR_2020;
|
||||
_nonFullHdrPixelFormat = MTLPixelFormatRGBA16Float;
|
||||
|
||||
// Initialize texture array to NULL
|
||||
for (int i = 0; i < MAX_VIDEO_PLANES; i++) {
|
||||
_cvMetalTextures[i] = NULL;
|
||||
@@ -393,16 +408,12 @@ CFStringRef __currentColorSpace;
|
||||
CFStringRef frame_trc = CFDictionaryGetValue(ext, kCVImageBufferTransferFunctionKey);
|
||||
if (CFEqual(frame_trc, kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ)) {
|
||||
isHDR = YES;
|
||||
if (@available(iOS 14.0, *)) {
|
||||
newColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_2100_PQ);
|
||||
} else {
|
||||
newColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_2020);
|
||||
}
|
||||
newPixelFormat = MTLPixelFormatBGR10A2Unorm;
|
||||
newColorSpace = CGColorSpaceCreateWithName(_nonFullHdrColorSpace);
|
||||
newPixelFormat = _nonFullHdrPixelFormat;
|
||||
} else {
|
||||
// SDR 2020, I'm not sure it's possible to stream this though
|
||||
newColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_2020);
|
||||
newPixelFormat = MTLPixelFormatBGR10A2Unorm;
|
||||
newColorSpace = CGColorSpaceCreateWithName(_nonFullHdrColorSpace);
|
||||
newPixelFormat = _nonFullHdrPixelFormat;
|
||||
}
|
||||
if (isHDR) {
|
||||
paramBuffer.cscParams = (fullRange ? k_CscParams_Bt2020Full_10bit : k_CscParams_Bt2020Lim_10bit);
|
||||
@@ -433,11 +444,11 @@ CFStringRef __currentColorSpace;
|
||||
Log(LOG_I,
|
||||
@"Frame pixel format %@ - changing MetalLayer's pixel format to %@",
|
||||
layer.pixelFormat == MTLPixelFormatBGRA8Unorm ? @"MTLPixelFormatBGRA8Unorm"
|
||||
: layer.pixelFormat == MTLPixelFormatBGR10A2Unorm ? @"MTLPixelFormatBGR10A2Unorm"
|
||||
: layer.pixelFormat == MTLPixelFormatBGRA10_XR ? @"MTLPixelFormatBGRA10_XR"
|
||||
: [NSString stringWithFormat:@"Unknown: %lu", layer.pixelFormat],
|
||||
newPixelFormat == MTLPixelFormatBGRA8Unorm ? @"MTLPixelFormatBGRA8Unorm"
|
||||
: newPixelFormat == MTLPixelFormatBGR10A2Unorm ? @"MTLPixelFormatBGR10A2Unorm"
|
||||
: [NSString stringWithFormat:@"Unknown: %lu", (unsigned long)layer.pixelFormat]);
|
||||
: newPixelFormat == MTLPixelFormatBGRA10_XR ? @"MTLPixelFormatBGRA10_XR"
|
||||
: [NSString stringWithFormat:@"Unknown: %lu", (unsigned long)newPixelFormat]);
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
@@ -489,6 +500,10 @@ CFStringRef __currentColorSpace;
|
||||
_lastFullRange = fullRange;
|
||||
}
|
||||
|
||||
|
||||
// NSLog(@"layer.pixelFormat %lu", (unsigned long)layer.pixelFormat);
|
||||
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@@ -557,29 +572,21 @@ CFStringRef __currentColorSpace;
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
- (void)renderFrame:(Frame *)frame withDrawable:(id<CAMetalDrawable>)drawable targetPresentationTimestamp:(CFTimeInterval)targetPresentationTimestamp API_AVAILABLE(ios(17.0)) {
|
||||
- (void)renderFrame:(Frame *)frame toLayer:(CAMetalLayer *)layer {
|
||||
@autoreleasepool {
|
||||
if (self.isStopping) {
|
||||
Log(LOG_I, @"[MetalVideoRenderer] isStopping");
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate accurate frametime using CAMetalDisplayLink timing
|
||||
if (self.lastPresented > 0.0f) {
|
||||
CFTimeInterval frametime = targetPresentationTimestamp - self.lastPresented;
|
||||
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:(frametime * 1000.0)];
|
||||
}
|
||||
self.lastPresented = targetPresentationTimestamp;
|
||||
|
||||
// Handle changes to the frame's colorspace from last time we rendered
|
||||
BOOL layerDidChange = NO;
|
||||
if (![self updateColorSpaceForFrame:frame toLayer:(CAMetalLayer *)drawable.layer layerDidChange:&layerDidChange]) {
|
||||
if (![self updateColorSpaceForFrame:frame toLayer:layer layerDidChange:&layerDidChange]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle changes to the video size or drawable size
|
||||
if (![self updateVideoRegionSizeForFrame:frame toLayer:(CAMetalLayer *)drawable.layer]) {
|
||||
if (![self updateVideoRegionSizeForFrame:frame toLayer:layer]) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -591,6 +598,15 @@ CFStringRef __currentColorSpace;
|
||||
}
|
||||
|
||||
size_t planes = CVPixelBufferGetPlaneCount(frame.pixelBuffer);
|
||||
|
||||
// For packed formats like BGRA, plane count is 0 but we treat it as 1 plane
|
||||
OSType pixelFormatType = CVPixelBufferGetPixelFormatType(frame.pixelBuffer);
|
||||
BOOL isPackedFormat = (pixelFormatType == kCVPixelFormatType_32BGRA ||
|
||||
pixelFormatType == kCVPixelFormatType_32ARGB);
|
||||
if (isPackedFormat) {
|
||||
planes = 1; // Treat packed formats as single plane
|
||||
}
|
||||
|
||||
assert(planes <= MAX_VIDEO_PLANES);
|
||||
|
||||
if (layerDidChange && frame.frameNumber > 1) {
|
||||
@@ -604,6 +620,13 @@ CFStringRef __currentColorSpace;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the next drawable early to get its pixel format
|
||||
id<CAMetalDrawable> drawable = [layer nextDrawable];
|
||||
if (!drawable) {
|
||||
Log(LOG_E, @"Failed to get nextDrawable");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the framebuffer pixel format for pipeline creation
|
||||
MTLPixelFormat framebufferPixelFormat = drawable.texture.pixelFormat;
|
||||
|
||||
@@ -622,16 +645,26 @@ CFStringRef __currentColorSpace;
|
||||
// linear shaders
|
||||
id<MTLFunction> yuvToLinear = [defaultLibrary newFunctionWithName:@"yuvToLinear"];
|
||||
|
||||
// Determine if this is 10-bit based on the framebuffer's pixel format
|
||||
BOOL is10Bit = (framebufferPixelFormat == MTLPixelFormatBGR10A2Unorm);
|
||||
// Determine if this is 10-bit based on the input CVPixelBuffer format, not the output framebuffer format
|
||||
// pixelFormatType is already declared above
|
||||
BOOL is10BitInput = (pixelFormatType == kCVPixelFormatType_420YpCbCr10BiPlanarFullRange ||
|
||||
pixelFormatType == kCVPixelFormatType_444YpCbCr10BiPlanarFullRange ||
|
||||
pixelFormatType == kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange ||
|
||||
pixelFormatType == kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange);
|
||||
|
||||
NSString *fragmentShaderName;
|
||||
if (planes == 2) {
|
||||
fragmentShaderName = is10Bit ? @"ps_draw_biplanar_10bit" : @"ps_draw_biplanar_8bit";
|
||||
if (isPackedFormat) {
|
||||
// BGRA/ARGB packed formats don't need color space conversion
|
||||
fragmentShaderName = @"ps_draw_bgra";
|
||||
} else if (planes == 2) {
|
||||
fragmentShaderName = is10BitInput ? @"ps_draw_biplanar_10bit" : @"ps_draw_biplanar_8bit";
|
||||
} else {
|
||||
fragmentShaderName = is10Bit ? @"ps_draw_triplanar_10bit" : @"ps_draw_triplanar_8bit";
|
||||
fragmentShaderName = is10BitInput ? @"ps_draw_triplanar_10bit" : @"ps_draw_triplanar_8bit";
|
||||
}
|
||||
|
||||
Log(LOG_I, @"DEBUG: CVPixelBuffer format: 0x%X, planes: %zu, is10BitInput: %d, shader: %@, framebuffer format: %lu",
|
||||
pixelFormatType, planes, is10BitInput, fragmentShaderName, (unsigned long)framebufferPixelFormat);
|
||||
|
||||
pipelineDesc.colorAttachments[0].pixelFormat = framebufferPixelFormat;
|
||||
pipelineDesc.vertexBuffers[0].mutability = MTLMutabilityImmutable;
|
||||
|
||||
@@ -659,41 +692,78 @@ CFStringRef __currentColorSpace;
|
||||
_videoPipelinePixelFormat[planes] = framebufferPixelFormat;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < planes; i++) {
|
||||
MTLPixelFormat fmt;
|
||||
|
||||
switch (CVPixelBufferGetPixelFormatType(frame.pixelBuffer)) {
|
||||
case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
|
||||
case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
|
||||
case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
|
||||
case kCVPixelFormatType_444YpCbCr8BiPlanarFullRange:
|
||||
fmt = (i == 0) ? MTLPixelFormatR8Unorm : MTLPixelFormatRG8Unorm;
|
||||
break;
|
||||
|
||||
case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
|
||||
case kCVPixelFormatType_444YpCbCr10BiPlanarFullRange:
|
||||
case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
|
||||
case kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange:
|
||||
fmt = (i == 0) ? MTLPixelFormatR16Unorm : MTLPixelFormatRG16Unorm;
|
||||
break;
|
||||
|
||||
default:
|
||||
Log(LOG_E, @"Unknown pixel format: %@", CVPixelBufferGetPixelFormatType(frame.pixelBuffer));
|
||||
return;
|
||||
if (isPackedFormat) {
|
||||
// Handle packed BGRA format - iOS/macOS uses BGRA internally
|
||||
// Check if the pixel buffer has IOSurface backing
|
||||
CFTypeRef ioSurface = CVPixelBufferGetIOSurface(frame.pixelBuffer);
|
||||
if (!ioSurface) {
|
||||
Log(LOG_E, @"CVPixelBuffer does not have IOSurface backing - cannot create Metal texture");
|
||||
return;
|
||||
}
|
||||
|
||||
CVReturn err = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
|
||||
_textureCache,
|
||||
frame.pixelBuffer,
|
||||
NULL,
|
||||
fmt,
|
||||
CVPixelBufferGetWidthOfPlane(frame.pixelBuffer, i),
|
||||
CVPixelBufferGetHeightOfPlane(frame.pixelBuffer, i),
|
||||
i,
|
||||
&_cvMetalTextures[i]);
|
||||
MTLPixelFormatBGRA8Unorm,
|
||||
CVPixelBufferGetWidth(frame.pixelBuffer),
|
||||
CVPixelBufferGetHeight(frame.pixelBuffer),
|
||||
0, // planeIndex must be 0 for non-planar
|
||||
&_cvMetalTextures[0]);
|
||||
if (err != kCVReturnSuccess) {
|
||||
Log(LOG_E, @"CVMetalTextureCacheCreateTextureFromImage() failed: %d", err);
|
||||
Log(LOG_E, @"CVMetalTextureCacheCreateTextureFromImage() failed for BGRA: %d", err);
|
||||
Log(LOG_E, @"PixelBuffer info - format: 0x%X, width: %zu, height: %zu, IOSurface: %p",
|
||||
pixelFormatType,
|
||||
CVPixelBufferGetWidth(frame.pixelBuffer),
|
||||
CVPixelBufferGetHeight(frame.pixelBuffer),
|
||||
ioSurface);
|
||||
return;
|
||||
} else {
|
||||
id<MTLTexture> texture = CVMetalTextureGetTexture(_cvMetalTextures[0]);
|
||||
Log(LOG_I, @"DEBUG: Created BGRA texture: format=%lu, width=%zu, height=%zu",
|
||||
(unsigned long)MTLPixelFormatBGRA8Unorm,
|
||||
CVPixelBufferGetWidth(frame.pixelBuffer),
|
||||
CVPixelBufferGetHeight(frame.pixelBuffer));
|
||||
}
|
||||
} else {
|
||||
// Handle planar YUV formats
|
||||
size_t actualPlanes = CVPixelBufferGetPlaneCount(frame.pixelBuffer);
|
||||
for (size_t i = 0; i < actualPlanes; i++) {
|
||||
MTLPixelFormat fmt;
|
||||
|
||||
switch (CVPixelBufferGetPixelFormatType(frame.pixelBuffer)) {
|
||||
case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
|
||||
case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
|
||||
case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
|
||||
case kCVPixelFormatType_444YpCbCr8BiPlanarFullRange:
|
||||
fmt = (i == 0) ? MTLPixelFormatR8Unorm : MTLPixelFormatRG8Unorm;
|
||||
break;
|
||||
|
||||
case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
|
||||
case kCVPixelFormatType_444YpCbCr10BiPlanarFullRange:
|
||||
case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
|
||||
case kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange:
|
||||
fmt = (i == 0) ? MTLPixelFormatR16Unorm : MTLPixelFormatRG16Unorm;
|
||||
break;
|
||||
|
||||
default:
|
||||
Log(LOG_E, @"Unknown pixel format: %@", CVPixelBufferGetPixelFormatType(frame.pixelBuffer));
|
||||
return;
|
||||
}
|
||||
|
||||
CVReturn err = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
|
||||
_textureCache,
|
||||
frame.pixelBuffer,
|
||||
NULL,
|
||||
fmt,
|
||||
CVPixelBufferGetWidthOfPlane(frame.pixelBuffer, i),
|
||||
CVPixelBufferGetHeightOfPlane(frame.pixelBuffer, i),
|
||||
i,
|
||||
&_cvMetalTextures[i]);
|
||||
if (err != kCVReturnSuccess) {
|
||||
Log(LOG_E, @"CVMetalTextureCacheCreateTextureFromImage() failed: %d", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -703,14 +773,26 @@ CFStringRef __currentColorSpace;
|
||||
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:_renderPassDescriptor];
|
||||
|
||||
[renderEncoder setRenderPipelineState:_videoPipelineState[planes]];
|
||||
for (size_t i = 0; i < planes; i++) {
|
||||
[renderEncoder setFragmentTexture:CVMetalTextureGetTexture(_cvMetalTextures[i]) atIndex:i];
|
||||
|
||||
if (isPackedFormat) {
|
||||
// For packed formats, we only have one texture
|
||||
[renderEncoder setFragmentTexture:CVMetalTextureGetTexture(_cvMetalTextures[0]) atIndex:0];
|
||||
} else {
|
||||
// For planar formats, set multiple textures
|
||||
size_t actualPlanes = CVPixelBufferGetPlaneCount(frame.pixelBuffer);
|
||||
for (size_t i = 0; i < actualPlanes; i++) {
|
||||
[renderEncoder setFragmentTexture:CVMetalTextureGetTexture(_cvMetalTextures[i]) atIndex:i];
|
||||
}
|
||||
}
|
||||
|
||||
[renderEncoder setVertexBuffer:_VideoVertexBuffer offset:0 atIndex:0];
|
||||
[renderEncoder setFragmentBuffer:_CscParamsBuffer offset:0 atIndex:0];
|
||||
|
||||
// Only set CSC params buffer for YUV formats that need color space conversion
|
||||
if (!isPackedFormat) {
|
||||
[renderEncoder setFragmentBuffer:_CscParamsBuffer offset:0 atIndex:0];
|
||||
}
|
||||
#if !TARGET_OS_TV
|
||||
if (((CAMetalLayer *)drawable.layer).pixelFormat == MTLPixelFormatRGBA16Float) {
|
||||
if (layer.pixelFormat == MTLPixelFormatRGBA16Float) {
|
||||
[self pollCurrentEDRHeadroom];
|
||||
[renderEncoder setFragmentBytes:&_currentEDRHeadroom length:sizeof(float) atIndex:1];
|
||||
}
|
||||
@@ -718,33 +800,29 @@ CFStringRef __currentColorSpace;
|
||||
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
|
||||
[renderEncoder endEncoding];
|
||||
|
||||
__block MetalVideoRenderer *strongSelf = self;
|
||||
#if !TARGET_OS_SIMULATOR
|
||||
[drawable addPresentedHandler:^(id<MTLDrawable> d) {
|
||||
if (strongSelf.lastPresented > 0.0f) {
|
||||
CFTimeInterval frametime = d.presentedTime - strongSelf.lastPresented;
|
||||
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:(frametime * 1000.0)];
|
||||
}
|
||||
strongSelf.lastPresented = d.presentedTime;
|
||||
}];
|
||||
#endif
|
||||
|
||||
// signal semaphore, compute GPU time average, and clear textures
|
||||
__block dispatch_semaphore_t block_semaphore = _inFlightSemaphore;
|
||||
__block size_t texturesToClean = isPackedFormat ? 1 : CVPixelBufferGetPlaneCount(frame.pixelBuffer);
|
||||
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> cb) {
|
||||
dispatch_semaphore_signal(block_semaphore);
|
||||
|
||||
if (cb.error) {
|
||||
Log(LOG_E, @"Command buffer error: %@", cb.error);
|
||||
// Request IDR frame to recover from rendering error
|
||||
LiRequestIdrFrame();
|
||||
|
||||
// Clear the command queue to reset GPU state
|
||||
if (cb.status == MTLCommandBufferStatusError) {
|
||||
NSError *error = cb.error;
|
||||
if (error.code == 2 || error.code == 4) { // GPU timeout or submissions ignored
|
||||
Log(LOG_W, @"GPU timeout detected, flushing command queue");
|
||||
// Force texture cache flush
|
||||
CVMetalTextureCacheFlush(self->_textureCache, 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const CFTimeInterval GPUTime = cb.GPUEndTime - cb.GPUStartTime;
|
||||
const double alpha = 0.25f;
|
||||
self->_averageGPUTime = (GPUTime * alpha) + (self->_averageGPUTime * (1.0 - alpha));
|
||||
}
|
||||
const CFTimeInterval GPUTime = cb.GPUEndTime - cb.GPUStartTime;
|
||||
const double alpha = 0.25f;
|
||||
self->_averageGPUTime = (GPUTime * alpha) + (self->_averageGPUTime * (1.0 - alpha));
|
||||
|
||||
// Free textures after completion of rendering
|
||||
for (size_t i = 0; i < planes; i++) {
|
||||
for (size_t i = 0; i < texturesToClean; i++) {
|
||||
if (self->_cvMetalTextures[i]) {
|
||||
CFRelease(self->_cvMetalTextures[i]);
|
||||
self->_cvMetalTextures[i] = NULL;
|
||||
@@ -754,27 +832,32 @@ CFStringRef __currentColorSpace;
|
||||
CVMetalTextureCacheFlush(self->_textureCache, 0);
|
||||
}];
|
||||
|
||||
// CAMetalDisplayLink handles timing automatically, so we don't use afterMinimumDuration
|
||||
#if TARGET_OS_SIMULATOR
|
||||
[commandBuffer presentDrawable:drawable];
|
||||
#else
|
||||
// present for a minimum duration for best frame pacing
|
||||
[commandBuffer presentDrawable:drawable afterMinimumDuration:1.0f / _framerate];
|
||||
#endif
|
||||
|
||||
[commandBuffer commit];
|
||||
[commandBuffer waitUntilCompleted];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
|
||||
- (BOOL)waitToRenderTo:(nonnull CAMetalLayer *)layer {
|
||||
// Wait to ensure only `MaxFramesInFlight` number of frames are getting processed
|
||||
// by any stage in the Metal pipeline (CPU, GPU, Metal, Drivers, etc.).
|
||||
if (!self.isStopping) {
|
||||
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)); // 100ms
|
||||
long result = dispatch_semaphore_wait(_inFlightSemaphore, timeout);
|
||||
if (result != 0) {
|
||||
Log(LOG_W, @"MetalVideoRenderer semaphore timeout - requesting IDR frame");
|
||||
LiRequestIdrFrame(); // Request recovery
|
||||
|
||||
// Force flush the texture cache when we timeout
|
||||
CVMetalTextureCacheFlush(_textureCache, 0);
|
||||
}
|
||||
if (self.isStopping) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)); // 100ms
|
||||
if (dispatch_semaphore_wait(_inFlightSemaphore, timeout) != 0) {
|
||||
Log(LOG_W, @"Timed out waiting for in-flight frame buffer.");
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)shutdown {
|
||||
|
||||
@@ -15,14 +15,13 @@
|
||||
@protocol MetalViewDelegate <NSObject>
|
||||
|
||||
- (void)drawableResize:(CGSize)size;
|
||||
- (void)renderWithDrawable:(nonnull id<CAMetalDrawable>)drawable toLayer:(nonnull CAMetalLayer *)layer API_AVAILABLE(ios(17.0));
|
||||
- (void)renderWithDrawable:(nonnull id<CAMetalDrawable>)drawable toLayer:(nonnull CAMetalLayer *)layer targetPresentationTimestamp:(CFTimeInterval)targetPresentationTimestamp API_AVAILABLE(ios(17.0));
|
||||
- (void)renderTo:(nonnull CAMetalLayer *)layer;
|
||||
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
|
||||
|
||||
@end
|
||||
|
||||
// The Metal game view base class.
|
||||
@interface MetalView : UIView <CALayerDelegate, CAMetalDisplayLinkDelegate>
|
||||
@interface MetalView : UIView <CALayerDelegate>
|
||||
|
||||
@property (nonatomic, nonnull, readonly) CAMetalLayer *metalLayer;
|
||||
@property (nonatomic, nullable) id<MetalViewDelegate> delegate;
|
||||
|
||||
@@ -11,12 +11,10 @@
|
||||
|
||||
#import "MetalView.h"
|
||||
#import "MetalConfig.h"
|
||||
#import <QuartzCore/CAMetalDisplayLink.h>
|
||||
|
||||
@implementation MetalView {
|
||||
// The secondary thread containing the render loop.
|
||||
NSThread *_renderThread;
|
||||
CAMetalDisplayLink *_metalDisplayLink API_AVAILABLE(ios(17.0));
|
||||
}
|
||||
|
||||
#pragma mark - Initialization and Setup.
|
||||
@@ -42,43 +40,15 @@
|
||||
self.layer.delegate = self;
|
||||
}
|
||||
|
||||
- (void)setFramerate:(float)framerate {
|
||||
_framerate = framerate;
|
||||
|
||||
if (@available(iOS 17.0, *)) {
|
||||
if (_metalDisplayLink) {
|
||||
_metalDisplayLink.preferredFrameRateRange = CAFrameRateRangeMake(framerate, framerate, framerate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)shutdown {
|
||||
// First cancel the thread to stop the run loop
|
||||
if (_renderThread) {
|
||||
Log(LOG_I, @"[MetalView] sending renderThread a cancel message");
|
||||
[_renderThread cancel];
|
||||
|
||||
// Invalidate metal display link to stop callbacks
|
||||
if (@available(iOS 17.0, *)) {
|
||||
if (_metalDisplayLink) {
|
||||
Log(LOG_I, @"[MetalView] invalidating metal display link");
|
||||
[_metalDisplayLink invalidate];
|
||||
_metalDisplayLink = nil;
|
||||
}
|
||||
}
|
||||
|
||||
// Now wait for thread to finish
|
||||
Log(LOG_I, @"[MetalView] waiting on renderThread to finish");
|
||||
NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:2.0]; // 2 second timeout
|
||||
while (!_renderThread.isFinished && [timeout timeIntervalSinceNow] > 0) {
|
||||
usleep(1000);
|
||||
}
|
||||
|
||||
if (_renderThread.isFinished) {
|
||||
Log(LOG_I, @"[MetalView] renderThread has finished");
|
||||
} else {
|
||||
Log(LOG_E, @"[MetalView] renderThread failed to finish in time");
|
||||
while (!_renderThread.isFinished) {
|
||||
usleep(100);
|
||||
}
|
||||
Log(LOG_I, @"[MetalView] renderThread has finished");
|
||||
_renderThread = nil;
|
||||
}
|
||||
}
|
||||
@@ -91,26 +61,6 @@
|
||||
[self movedToWindow];
|
||||
}
|
||||
|
||||
- (void)metalDisplayLink:(CAMetalDisplayLink *)link needsUpdate:(CAMetalDisplayLinkUpdate *)update API_AVAILABLE(ios(17.0)) {
|
||||
// Skip rendering if we're shutting down
|
||||
if ([NSThread currentThread].isCancelled) {
|
||||
return;
|
||||
}
|
||||
@autoreleasepool {
|
||||
[self.delegate waitToRenderTo:self.metalLayer];
|
||||
|
||||
id<CAMetalDrawable> drawable = update.drawable;
|
||||
if (drawable) {
|
||||
// Pass timing information to the renderer
|
||||
if ([self.delegate respondsToSelector:@selector(renderWithDrawable:toLayer:targetPresentationTimestamp:)]) {
|
||||
[self.delegate renderWithDrawable:drawable toLayer:self.metalLayer targetPresentationTimestamp:update.targetPresentationTimestamp];
|
||||
} else {
|
||||
[self.delegate renderWithDrawable:drawable toLayer:self.metalLayer];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)movedToWindow {
|
||||
if (!self.window) {
|
||||
Log(LOG_I, @"[MetalView] movedToWindow(nil): shutting down...");
|
||||
@@ -118,32 +68,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (@available(iOS 17.0, *)) {
|
||||
_metalDisplayLink = [[CAMetalDisplayLink alloc] initWithMetalLayer:_metalLayer];
|
||||
_metalDisplayLink.delegate = self;
|
||||
_metalDisplayLink.preferredFrameRateRange = CAFrameRateRangeMake(_framerate, _framerate, _framerate);
|
||||
Log(LOG_I, @"[MetalView] Using CAMetalDisplayLink for optimal Metal rendering");
|
||||
}
|
||||
|
||||
// Start the display link on a background thread
|
||||
// Render on a new thread
|
||||
_renderThread = [[NSThread alloc] initWithBlock:^{
|
||||
// Add metal display link to this thread's run loop
|
||||
if (@available(iOS 17.0, *)) {
|
||||
[self->_metalDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
|
||||
}
|
||||
|
||||
// Keep the run loop alive, checking for cancellation regularly
|
||||
while (![NSThread currentThread].isCancelled && self->_metalDisplayLink) {
|
||||
while (![NSThread currentThread].isCancelled) {
|
||||
@autoreleasepool {
|
||||
// Run the run loop for a short time to allow checking cancellation
|
||||
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from run loop if still valid
|
||||
if (@available(iOS 17.0, *)) {
|
||||
if (self->_metalDisplayLink) {
|
||||
[self->_metalDisplayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
|
||||
[self.delegate waitToRenderTo:self.metalLayer];
|
||||
[self.delegate renderTo:self.metalLayer];
|
||||
}
|
||||
}
|
||||
Log(LOG_I, @"[MetalView] renderThread is exiting");
|
||||
@@ -151,7 +81,7 @@
|
||||
_renderThread.name = @"MetalVideoRenderer";
|
||||
_renderThread.qualityOfService = NSQualityOfServiceUserInteractive;
|
||||
[_renderThread start];
|
||||
Log(LOG_I, @"[MetalView] started renderThread with CAMetalDisplayLink at %f fps", _framerate);
|
||||
Log(LOG_I, @"[MetalView] started renderThread %@", _renderThread);
|
||||
|
||||
// Perform any actions that need to know the size and scale of the drawable. When UIKit calls
|
||||
// didMoveToWindow after the view initialization, this is the first opportunity to notify
|
||||
|
||||
@@ -12,15 +12,14 @@
|
||||
#import "ImGuiRenderer.h"
|
||||
#import "MetalVideoRenderer.h"
|
||||
#import "MetalView.h"
|
||||
#import "TemporarySettings.h"
|
||||
|
||||
@interface MetalViewController : UIViewController <MetalViewDelegate>
|
||||
|
||||
@property (nonatomic) CGRect bounds;
|
||||
|
||||
- (nonnull instancetype)initWithFrame:(CGRect)bounds
|
||||
framerate:(float)framerate
|
||||
enableHdr:(BOOL)enableHdr
|
||||
metricsHandler:(MetricsHandler _Nonnull)metricsHandler;
|
||||
- (nonnull instancetype)initWithFrame:(CGRect)bounds framerate:(float)framerate settings:(TemporarySettings* _Nonnull )settings metricsHandler:(MetricsHandler _Nonnull)metricsHandler;
|
||||
|
||||
|
||||
- (void)pauseRendering;
|
||||
- (void)resumeRendering;
|
||||
|
||||
@@ -14,19 +14,21 @@
|
||||
@implementation MetalViewController {
|
||||
FrameQueue *_frameQueue;
|
||||
float _framerate;
|
||||
BOOL _enableHdr;
|
||||
TemporarySettings* _currentSettings;
|
||||
MetalView *_metalView;
|
||||
MetalVideoRenderer *_renderer;
|
||||
MetricsHandler _metricsHandler;
|
||||
CADisplayLink *_displayLink;
|
||||
BOOL _frameSlotAcquired;
|
||||
}
|
||||
|
||||
- (nonnull instancetype)initWithFrame:(CGRect)bounds framerate:(float)framerate enableHdr:(BOOL)enableHdr metricsHandler:(MetricsHandler)metricsHandler {
|
||||
- (nonnull instancetype)initWithFrame:(CGRect)bounds framerate:(float)framerate settings:(TemporarySettings* )settings metricsHandler:(MetricsHandler)metricsHandler {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_bounds = bounds;
|
||||
_frameQueue = [FrameQueue sharedInstance];
|
||||
_framerate = framerate;
|
||||
_enableHdr = enableHdr;
|
||||
_currentSettings = settings;
|
||||
_metricsHandler = metricsHandler;
|
||||
}
|
||||
return self;
|
||||
@@ -58,11 +60,28 @@
|
||||
}
|
||||
view.metalLayer.device = device;
|
||||
|
||||
// Determine supported pixel format before initializing renderer
|
||||
// Use TARGET_OS_SIMULATOR to detect simulator environment
|
||||
MTLPixelFormat pixelFormat;
|
||||
#if TARGET_OS_SIMULATOR
|
||||
// iOS Simulator doesn't support BGRA10_XR
|
||||
pixelFormat = MTLPixelFormatBGRA8Unorm;
|
||||
Log(LOG_W, @"Running on iOS Simulator, using BGRA8Unorm pixel format");
|
||||
#else
|
||||
// On real devices, check if we should enable HDR
|
||||
if (_currentSettings.enableHdr) {
|
||||
pixelFormat = MTLPixelFormatBGRA10_XR;
|
||||
Log(LOG_I, @"HDR enabled, using BGRA10_XR pixel format");
|
||||
} else {
|
||||
pixelFormat = MTLPixelFormatBGRA8Unorm;
|
||||
Log(LOG_I, @"HDR disabled, using BGRA8Unorm pixel format");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Initialize the renderer.
|
||||
MetalVideoRenderer *renderer = [[MetalVideoRenderer alloc] initWithMetalDevice:device
|
||||
drawablePixelFormat:MTLPixelFormatBGR10A2Unorm
|
||||
framerate:self->_framerate
|
||||
hdrEnabled:self->_enableHdr];
|
||||
drawablePixelFormat:pixelFormat
|
||||
settings:_currentSettings];
|
||||
if (!renderer) {
|
||||
Log(LOG_E, @"The renderer couldn't be initialized.");
|
||||
return;
|
||||
@@ -73,6 +92,20 @@
|
||||
// Initialize the renderer-dependent view properties.
|
||||
view.metalLayer.pixelFormat = renderer.colorPixelFormat;
|
||||
view.metalLayer.maximumDrawableCount = 3;
|
||||
|
||||
// We need a no-op displaylink timer or iOS can decide to run at 60fps
|
||||
// The overhead from this should be minimal.
|
||||
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkHandler:)];
|
||||
if (@available(iOS 15.0, tvOS 15.0, *)) {
|
||||
_displayLink.preferredFrameRateRange = CAFrameRateRangeMake(_framerate, _framerate, _framerate);
|
||||
} else {
|
||||
_displayLink.preferredFramesPerSecond = _framerate;
|
||||
}
|
||||
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
|
||||
}
|
||||
|
||||
- (void)displayLinkHandler:(CADisplayLink *)link {
|
||||
// Rendering does not use DisplayLink, this exists to fool iOS into keeping us running at the desired framerate
|
||||
}
|
||||
|
||||
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
|
||||
@@ -81,36 +114,36 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Renderer obtains a nextDrawable, waiting if necessary
|
||||
if (@available(iOS 13.0, *)) {
|
||||
[_renderer waitToRenderTo:layer];
|
||||
_frameSlotAcquired = [_renderer waitToRenderTo:layer];
|
||||
}
|
||||
|
||||
// If we don't have a frame yet, wait on that too
|
||||
[_frameQueue waitForEnqueue];
|
||||
if (_frameSlotAcquired) {
|
||||
[_frameQueue waitForEnqueue];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)renderWithDrawable:(nonnull id<CAMetalDrawable>)drawable toLayer:(nonnull CAMetalLayer *)layer API_AVAILABLE(ios(17.0)) {
|
||||
/// Draw frame (used by manual loop)
|
||||
- (void)renderTo:(nonnull CAMetalLayer *)layer {
|
||||
if (!_frameSlotAcquired) {
|
||||
return;
|
||||
}
|
||||
CFTimeInterval timeout = (1.0f / _framerate) - _renderer.averageGPUTime;
|
||||
Frame *frame = [_frameQueue dequeueWithTimeout:timeout];
|
||||
Frame *frame = [_frameQueue dequeueWithTimeoutSync:timeout];
|
||||
|
||||
if (!_renderer.isStopping) {
|
||||
// Only render if not paused
|
||||
if (frame) {
|
||||
[_renderer renderFrame:frame withDrawable:drawable];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)renderWithDrawable:(nonnull id<CAMetalDrawable>)drawable toLayer:(nonnull CAMetalLayer *)layer targetPresentationTimestamp:(CFTimeInterval)targetPresentationTimestamp API_AVAILABLE(ios(17.0)) {
|
||||
if (!_renderer.isStopping) {
|
||||
CFTimeInterval timeout = (1.0f / _framerate) - _renderer.averageGPUTime;
|
||||
Frame *frame = [_frameQueue dequeueWithTimeout:timeout];
|
||||
if (frame) {
|
||||
[_renderer renderFrame:frame withDrawable:drawable targetPresentationTimestamp:targetPresentationTimestamp];
|
||||
//if (@available(iOS 13.0, *)) {
|
||||
[_renderer renderFrame:frame toLayer:layer];
|
||||
//}
|
||||
} else {
|
||||
dispatch_semaphore_signal([_renderer inFlightSemaphore]);
|
||||
}
|
||||
} else {
|
||||
// When paused, we still dequeue frames to prevent accumulation
|
||||
// but don't render them. Also sleep a bit to reduce CPU usage
|
||||
dispatch_semaphore_signal([_renderer inFlightSemaphore]);
|
||||
usleep(100000);
|
||||
}
|
||||
}
|
||||
@@ -120,6 +153,9 @@
|
||||
}
|
||||
|
||||
- (void)pauseRendering {
|
||||
if (_displayLink) {
|
||||
_displayLink.paused = YES;
|
||||
}
|
||||
if (_renderer) {
|
||||
_renderer.isStopping = YES;
|
||||
}
|
||||
@@ -130,6 +166,9 @@
|
||||
if (_renderer) {
|
||||
_renderer.isStopping = NO;
|
||||
}
|
||||
if (_displayLink) {
|
||||
_displayLink.paused = NO;
|
||||
}
|
||||
Log(LOG_I, @"[MetalViewController] Rendering resumed");
|
||||
}
|
||||
|
||||
@@ -138,13 +177,16 @@
|
||||
|
||||
Log(LOG_I, @"[MetalViewController] viewDidDisappear");
|
||||
|
||||
// Shutdown the renderer first
|
||||
if (_displayLink) {
|
||||
[_displayLink invalidate];
|
||||
_displayLink = nil;
|
||||
}
|
||||
|
||||
if (_renderer) {
|
||||
[_renderer shutdown];
|
||||
_renderer = nil;
|
||||
}
|
||||
|
||||
// Then shutdown the view's display link
|
||||
if (_metalView) {
|
||||
_metalView.delegate = nil;
|
||||
[_metalView shutdown];
|
||||
|
||||
@@ -127,3 +127,11 @@ fragment float4 ps_draw_triplanar_10bit(Vertex v [[ stage_in ]],
|
||||
rgb.b = dot(yuv_corrected, cscParams.matrix[2]);
|
||||
return float4(rgb, 1.0f);
|
||||
}
|
||||
|
||||
// Shader for packed BGRA format (no color space conversion needed)
|
||||
fragment float4 ps_draw_bgra(Vertex v [[ stage_in ]],
|
||||
texture2d<float> bgraTexture [[ texture(0) ]])
|
||||
{
|
||||
// BGRA format is already in RGB color space, just sample and return
|
||||
return bgraTexture.sample(s, v.texCoords);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
NSString* _uniqueId;
|
||||
NSData* _cert;
|
||||
BOOL shouldDiscover;
|
||||
DataManager* dataMan;
|
||||
}
|
||||
|
||||
- (id)initWithHosts:(NSArray *)hosts andCallback:(id<DiscoveryCallback>)callback {
|
||||
@@ -52,6 +53,7 @@
|
||||
[CryptoManager generateKeyPairUsingSSL];
|
||||
_uniqueId = [IdManager getUniqueId];
|
||||
_cert = [CryptoManager readCertFromFile];
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -139,6 +141,8 @@
|
||||
if ([serverInfoResponse isStatusOk]) {
|
||||
host = [[TemporaryHost alloc] init];
|
||||
host.activeAddress = host.address = hostAddress;
|
||||
if(!host.activeAddressPool) host.activeAddressPool = [[NSMutableSet alloc] init];
|
||||
[host.activeAddressPool addObject:host.activeAddress];
|
||||
host.state = StateOnline;
|
||||
[serverInfoResponse populateHost:host];
|
||||
|
||||
@@ -284,8 +288,9 @@
|
||||
existingHost.externalAddress = host.externalAddress;
|
||||
}
|
||||
existingHost.activeAddress = host.activeAddress;
|
||||
[existingHost.activeAddressPool addObject:host.activeAddress];
|
||||
existingHost.state = host.state;
|
||||
return NO;
|
||||
return NO; // returning a "NO" here means Updating addresses of existing host
|
||||
}
|
||||
else {
|
||||
@synchronized (_hostQueue) {
|
||||
|
||||
@@ -41,11 +41,22 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
||||
}
|
||||
|
||||
- (NSArray*) getHostAddressList {
|
||||
NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:3];
|
||||
NSMutableArray *array = [[NSMutableArray alloc] init];
|
||||
|
||||
if (_host.localAddress != nil) {
|
||||
[array addObject:_host.localAddress];
|
||||
}
|
||||
|
||||
NSArray *poolSnapshot = [_host.activeAddressPool copy];
|
||||
for (NSString *address in poolSnapshot) {
|
||||
if ([address containsString:@":"]) break;
|
||||
[array addObject:address];
|
||||
}
|
||||
for (NSString *address in poolSnapshot) {
|
||||
if (![address containsString:@":"]) break;
|
||||
[array addObject:address];
|
||||
}
|
||||
|
||||
if (_host.address != nil) {
|
||||
[array addObject:_host.address];
|
||||
}
|
||||
@@ -61,6 +72,7 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
||||
// to preserve insertion order of addresses.
|
||||
for (int i = 0; i < [array count]; i++) {
|
||||
NSString *addr1 = [array objectAtIndex:i];
|
||||
// NSLog(@"iterates discovery addrs %@, host: %@", addr1, _host.name);
|
||||
|
||||
for (int j = 1; j < [array count]; j++) {
|
||||
if (i == j) {
|
||||
@@ -104,6 +116,8 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
||||
receivedResponse = [self checkResponse:serverInfoResp];
|
||||
if (receivedResponse) {
|
||||
_host.activeAddress = address;
|
||||
if(!_host.activeAddressPool) _host.activeAddressPool = [[NSMutableSet alloc] init];
|
||||
[_host.activeAddressPool addObject:address];
|
||||
[serverInfoResp populateHost:_host];
|
||||
|
||||
// Update the database using the response
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
- (NSURLRequest*) newLaunchOrResumeRequest:(NSString*)verb config:(StreamConfiguration*)config;
|
||||
- (NSURLRequest*) newQuitAppRequest;
|
||||
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString*)appId;
|
||||
- (NSURLRequest*) newBirateRequest:(NSInteger)bitrateKbps forClient:(NSString* )clientName;
|
||||
- (void) executeRequestSynchronously:(HttpRequest*)request;
|
||||
|
||||
@end
|
||||
|
||||
@@ -231,6 +231,7 @@
|
||||
}
|
||||
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/applist?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
||||
NSLog(@"urlString: %@", urlString);
|
||||
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
@@ -292,9 +293,22 @@
|
||||
}
|
||||
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/cancel?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
||||
NSLog(@"urlString print: %@", urlString);
|
||||
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest*)newBirateRequest:(NSInteger)bitrateKbps forClient:(NSString* )clientName {
|
||||
if (![self ensureHttpsUrlPopulated:NO]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/bitrate?clientname=%@&bitrate=%@&uniqueid=%@", _baseHTTPSURL, clientName, [NSString stringWithFormat:@"%ld", (long)bitrateKbps], _uniqueId];
|
||||
|
||||
NSLog(@"bitrate urlString print: %@", urlString);
|
||||
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
|
||||
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString *)appId {
|
||||
if (![self ensureHttpsUrlPopulated:NO]) {
|
||||
return nil;
|
||||
|
||||
@@ -228,6 +228,7 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
||||
Log(LOG_I, @"IPv6 address chosen: %@ -> %@", [service hostName], host.ipv6Address);
|
||||
|
||||
host.activeAddress = host.localAddress;
|
||||
// [host.activeAddressPool addObject:host.localAddress];
|
||||
host.name = service.hostName;
|
||||
[self.callback updateHost:host];
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
// If we reached this host through this port, store our port there
|
||||
if (host.activeAddress && [lanAddr isEqualToString:[Utils addressPortStringToAddress:host.activeAddress]]) {
|
||||
localPort = [Utils addressPortStringToPort:host.activeAddress];
|
||||
[host.activeAddressPool addObject:host.activeAddress];
|
||||
}
|
||||
else if (host.localAddress) {
|
||||
// If there's an existing local address, use the port from that
|
||||
@@ -57,6 +58,7 @@
|
||||
}
|
||||
|
||||
host.localAddress = [Utils addressAndPortToAddressPortString:lanAddr port:localPort];
|
||||
[host.activeAddressPool addObject:lanAddr];
|
||||
}
|
||||
|
||||
// This is a Sunshine extension for WAN port remapping
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" fill="none" viewBox="0 0 120 120">
|
||||
<g clip-path="url(#a)">
|
||||
<g filter="url(#b)">
|
||||
<path fill="#fff" fill-opacity="1"
|
||||
d="M67.25 10.437c.099.075.215.123.338.142a50 50 0 0 1 27.233 13.54.74.74 0 0 1 .004 1.056l-.776.775a.76.76 0 0 1-1.065.004 47.405 47.405 0 0 0-65.97 0 .76.76 0 0 1-1.064-.004l-.776-.775a.74.74 0 0 1 .004-1.057A50 50 0 0 1 52.41 10.58a.76.76 0 0 0 .339-.142l6.8-5.1a.75.75 0 0 1 .9 0z"/>
|
||||
</g>
|
||||
<g filter="url(#c)">
|
||||
<path fill="#fff" fill-opacity="1"
|
||||
d="M61.955 6.804c2.487 2.295 5.657 3.663 8.96 4.402a50 50 0 0 1 23.51 12.533c.52.493.52 1.316.013 1.823a1.326 1.326 0 0 1-1.849.013 47.403 47.403 0 0 0-65.179 0 1.326 1.326 0 0 1-1.848-.013 1.27 1.27 0 0 1 .012-1.823 50 50 0 0 1 23.512-12.533c3.302-.739 6.47-2.106 8.958-4.401L60 5z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<!-- defs stay the same -->
|
||||
<filter id="b" width="82.089" height="32.983" x="18.955" y="-.813" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur result="effect1_foregroundBlur_211_1847" stdDeviation="3"/>
|
||||
</filter>
|
||||
<filter id="c" width="69.634" height="22.444" x="25.183" y="5" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"/>
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_211_1847"/>
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_211_1847" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h120v120H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg width="860" height="860" viewBox="0 0 860 860" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3_30)">
|
||||
<path opacity="0.15" d="M430 45.3164C642.136 45.3164 814.471 217.345 814.471 430C814.471 642.656 642.136 814.684 430 814.684C217.864 814.684 45.5294 642.656 45.5294 430C45.5294 217.345 217.864 45.3164 430 45.3164Z" stroke="white" stroke-width="90"/>
|
||||
<path opacity="0.15" d="M430 122.855L508.859 213.488H351.141L430 122.855Z" fill="#D9D9D9"/>
|
||||
<path opacity="0.15" d="M430 737.144L351.141 646.512H508.859L430 737.144Z" fill="#D9D9D9"/>
|
||||
<path opacity="0.15" d="M738.588 430.001L647.529 508.491V351.512L738.588 430.001Z" fill="#D9D9D9"/>
|
||||
<path opacity="0.15" d="M121.412 430.001L212.471 351.512V508.491L121.412 430.001Z" fill="#D9D9D9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3_30">
|
||||
<rect width="860" height="860" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 879 B |
@@ -0,0 +1,14 @@
|
||||
<svg width="860" height="860" viewBox="0 0 860 860" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3_30)">
|
||||
<path opacity="0.75" d="M430 45.3164C642.136 45.3164 814.471 217.345 814.471 430C814.471 642.656 642.136 814.684 430 814.684C217.864 814.684 45.5294 642.656 45.5294 430C45.5294 217.345 217.864 45.3164 430 45.3164Z" stroke="white" stroke-width="90"/>
|
||||
<path opacity="0.75" d="M430 122.855L508.859 213.488H351.141L430 122.855Z" fill="#D9D9D9"/>
|
||||
<path opacity="0.75" d="M430 737.144L351.141 646.512H508.859L430 737.144Z" fill="#D9D9D9"/>
|
||||
<path opacity="0.75" d="M738.588 430.001L647.529 508.491V351.512L738.588 430.001Z" fill="#D9D9D9"/>
|
||||
<path opacity="0.75" d="M121.412 430.001L212.471 351.512V508.491L121.412 430.001Z" fill="#D9D9D9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3_30">
|
||||
<rect width="860" height="860" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 879 B |
@@ -0,0 +1,14 @@
|
||||
<svg width="860" height="860" viewBox="0 0 860 860" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3_30)">
|
||||
<path opacity="0.25" d="M430 45.3164C642.136 45.3164 814.471 217.345 814.471 430C814.471 642.656 642.136 814.684 430 814.684C217.864 814.684 45.5294 642.656 45.5294 430C45.5294 217.345 217.864 45.3164 430 45.3164Z" stroke="white" stroke-width="90"/>
|
||||
<path opacity="0.25" d="M430 122.855L508.859 213.488H351.141L430 122.855Z" fill="#D9D9D9"/>
|
||||
<path opacity="0.25" d="M430 737.144L351.141 646.512H508.859L430 737.144Z" fill="#D9D9D9"/>
|
||||
<path opacity="0.25" d="M738.588 430.001L647.529 508.491V351.512L738.588 430.001Z" fill="#D9D9D9"/>
|
||||
<path opacity="0.25" d="M121.412 430.001L212.471 351.512V508.491L121.412 430.001Z" fill="#D9D9D9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3_30">
|
||||
<rect width="860" height="860" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 879 B |
@@ -0,0 +1,34 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" fill="none" viewBox="0 0 120 120">
|
||||
<g clip-path="url(#a)">
|
||||
<g filter="url(#b)">
|
||||
<path fill="#fff" fill-opacity="1"
|
||||
d="M67.25 10.437c.099.075.215.123.338.142a50 50 0 0 1 27.233 13.54.74.74 0 0 1 .004 1.056l-.776.775a.76.76 0 0 1-1.065.004 47.405 47.405 0 0 0-65.97 0 .76.76 0 0 1-1.064-.004l-.776-.775a.74.74 0 0 1 .004-1.057A50 50 0 0 1 52.41 10.58a.76.76 0 0 0 .339-.142l6.8-5.1a.75.75 0 0 1 .9 0z"/>
|
||||
</g>
|
||||
<g filter="url(#c)">
|
||||
<path fill="#fff" fill-opacity="1"
|
||||
d="M61.955 6.804c2.487 2.295 5.657 3.663 8.96 4.402a50 50 0 0 1 23.51 12.533c.52.493.52 1.316.013 1.823a1.326 1.326 0 0 1-1.849.013 47.403 47.403 0 0 0-65.179 0 1.326 1.326 0 0 1-1.848-.013 1.27 1.27 0 0 1 .012-1.823 50 50 0 0 1 23.512-12.533c3.302-.739 6.47-2.106 8.958-4.401L60 5z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<!-- defs stay the same -->
|
||||
<filter id="b" width="82.089" height="32.983" x="18.955" y="-.813" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur result="effect1_foregroundBlur_211_1847" stdDeviation="3"/>
|
||||
</filter>
|
||||
<filter id="c" width="69.634" height="22.444" x="25.183" y="5" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"/>
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_211_1847"/>
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow_211_1847" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h120v120H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -5,6 +5,10 @@
|
||||
// Created by Diego Waxemberg on 1/19/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2025.9
|
||||
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "ConnectionCallbacks.h"
|
||||
#import "VideoDecoderRenderer.h"
|
||||
@@ -15,6 +19,8 @@
|
||||
#define CONN_TEST_SERVER "www.baidu.com"
|
||||
|
||||
@interface Connection : NSOperation <NSStreamDelegate>
|
||||
@property (class, nonatomic, assign) bool muteInBackground;
|
||||
@property (class, nonatomic, assign) bool useSystemAudioEngine;
|
||||
|
||||
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks;
|
||||
-(void) terminate;
|
||||
@@ -23,4 +29,6 @@
|
||||
-(BOOL) getVideoStats:(video_stats_t*)stats;
|
||||
-(NSString*) getActiveCodecName;
|
||||
|
||||
+ (void)setVolume:(float)newVolume;
|
||||
|
||||
@end
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
// Created by Diego Waxemberg on 1/19/14.
|
||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2025.9
|
||||
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Connection.h"
|
||||
#import "Plot.h"
|
||||
@@ -18,6 +21,7 @@
|
||||
|
||||
#include "Limelight.h"
|
||||
#include "opus_multistream.h"
|
||||
#include "VoidLink-Swift.h"
|
||||
|
||||
@implementation Connection {
|
||||
SERVER_INFORMATION _serverInfo;
|
||||
@@ -43,15 +47,26 @@ static NSLock* videoStatsLock;
|
||||
static SDL_AudioDeviceID audioDevice;
|
||||
static OPUS_MULTISTREAM_CONFIGURATION audioConfig;
|
||||
static void* audioBuffer;
|
||||
static float volume = 1.0;
|
||||
static int audioFrameSize;
|
||||
|
||||
static bool useSystemAudioEngine;
|
||||
static bool audioSessionInterrupted;
|
||||
static AVAudioEngine *audioEngine;
|
||||
static AVAudioPlayerNode *audioPlayerNode;
|
||||
static AVAudioPCMBuffer *pcmBuffer;
|
||||
static AVAudioFormat *audioFormat;
|
||||
|
||||
static bool muteInBackground;
|
||||
static bool fullColorRange;
|
||||
|
||||
static VideoDecoderRenderer* renderer;
|
||||
|
||||
static BandwidthTracker *bwTracker;
|
||||
|
||||
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
|
||||
{
|
||||
[renderer setupWithVideoFormat:videoFormat width:width height:height frameRate:redrawRate];
|
||||
[renderer setupWithVideoFormat:videoFormat width:width height:height frameRate:redrawRate fullRange:fullColorRange];
|
||||
lastFrameNumber = 0;
|
||||
activeVideoFormat = videoFormat;
|
||||
Log(LOG_I, @"Active video format: 0x%x", activeVideoFormat);
|
||||
@@ -273,10 +288,26 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
|
||||
// Start playback
|
||||
SDL_PauseAudioDevice(audioDevice, 0);
|
||||
|
||||
// Disable lowering volume of other audio streams (SDL sets AVAudioSessionCategoryOptionDuckOthers by default)
|
||||
// [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];
|
||||
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];
|
||||
// return 0;
|
||||
|
||||
// System audio engine initialization
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
TemporarySettings* tempSettings = [dataMan getSettings];
|
||||
bool useBluetoothD2P = tempSettings.useBuiltinMic || !tempSettings.redirectMic;
|
||||
AVAudioSessionCategoryOptions bluetoothAudioOption = useBluetoothD2P ? AVAudioSessionCategoryOptionAllowBluetoothA2DP : AVAudioSessionCategoryOptionAllowBluetooth;
|
||||
AVAudioSessionCategoryOptions volumeMixOption = tempSettings.duckOtherApps ? AVAudioSessionCategoryOptionDuckOthers : AVAudioSessionCategoryOptionMixWithOthers;
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
[session setCategory:tempSettings.redirectMic ? AVAudioSessionCategoryPlayAndRecord : AVAudioSessionCategoryPlayback
|
||||
mode:AVAudioSessionModeDefault
|
||||
options:volumeMixOption|bluetoothAudioOption
|
||||
error:nil];
|
||||
if(tempSettings.redirectMic) if(@available(iOS 13.0, *)) [session setAllowHapticsAndSystemSoundsDuringRecording:YES error:nil];
|
||||
[session setActive:YES error:nil];
|
||||
audioSessionInterrupted = false;
|
||||
|
||||
AudioEngineInit(audioConfig.sampleRate, audioConfig.channelCount);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -301,8 +332,78 @@ void ArCleanup(void)
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
}
|
||||
|
||||
+ (void)setVolume:(float)linearVolume{
|
||||
if (linearVolume <= 0.0f) linearVolume = 0.0f;
|
||||
if (linearVolume >= 1.0f) linearVolume = 1.0f;
|
||||
CGFloat exponent = 2.5;
|
||||
volume = powf(linearVolume, exponent);
|
||||
}
|
||||
|
||||
+ (void)setMuteInBackground:(bool)mute {
|
||||
muteInBackground = mute;
|
||||
}
|
||||
|
||||
+ (void)setUseSystemAudioEngine:(bool)useSysAudioEngine{
|
||||
useSystemAudioEngine = useSysAudioEngine;
|
||||
}
|
||||
|
||||
void AudioEngineInit(int sampleRate, int channelCount) {
|
||||
|
||||
audioEngine = [[AVAudioEngine alloc] init];
|
||||
audioPlayerNode = [[AVAudioPlayerNode alloc] init];
|
||||
|
||||
[audioEngine attachNode:audioPlayerNode];
|
||||
|
||||
AVAudioChannelLayout *layout;
|
||||
|
||||
switch (channelCount) {
|
||||
case 2:
|
||||
default:
|
||||
audioFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
|
||||
sampleRate:sampleRate
|
||||
channels:channelCount
|
||||
interleaved:NO];
|
||||
break;
|
||||
case 6:
|
||||
layout =
|
||||
[[AVAudioChannelLayout alloc] initWithLayoutTag:kAudioChannelLayoutTag_MPEG_5_1_A];
|
||||
audioFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
|
||||
sampleRate:sampleRate
|
||||
interleaved:NO
|
||||
channelLayout:layout];
|
||||
break;
|
||||
case 8:
|
||||
layout =
|
||||
[[AVAudioChannelLayout alloc] initWithLayoutTag:kAudioChannelLayoutTag_MPEG_7_1_A];
|
||||
audioFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
|
||||
sampleRate:sampleRate
|
||||
interleaved:NO
|
||||
channelLayout:layout];
|
||||
break;
|
||||
}
|
||||
|
||||
if(!audioFormat) return;
|
||||
|
||||
AVAudioFormat *mixerFormat = [audioEngine.mainMixerNode outputFormatForBus:0];
|
||||
AVAudioFormat *outputFormat = [audioEngine.outputNode inputFormatForBus:0];
|
||||
NSLog(@"Mixer: %dch, Output: %dch",
|
||||
(int)mixerFormat.channelCount,
|
||||
(int)outputFormat.channelCount);
|
||||
|
||||
[audioEngine connect:audioPlayerNode to:audioEngine.mainMixerNode format:audioFormat];
|
||||
|
||||
NSError *err = nil;
|
||||
if (![audioEngine startAndReturnError:&err]) {
|
||||
NSLog(@"AudioEngine start error: %@", err);
|
||||
}
|
||||
|
||||
[audioPlayerNode play];
|
||||
}
|
||||
|
||||
void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
{
|
||||
if(appDidEnterBackgroundWithoutPip && muteInBackground) return;
|
||||
|
||||
int decodeLen;
|
||||
|
||||
// Don't queue if there's already more than 30 ms of audio data waiting
|
||||
@@ -317,18 +418,49 @@ void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
(float*)audioBuffer,
|
||||
audioConfig.samplesPerFrame,
|
||||
0);
|
||||
|
||||
if (decodeLen > 0) {
|
||||
// Provide backpressure on the queue to ensure too many frames don't build up
|
||||
// in SDL's audio queue.
|
||||
while (SDL_GetQueuedAudioSize(audioDevice) / audioFrameSize > 10) {
|
||||
[NSThread sleepForTimeInterval:0.001f];
|
||||
|
||||
float* fbuf = (float*)audioBuffer;
|
||||
|
||||
if(useSystemAudioEngine){
|
||||
// 创建 AVAudioPCMBuffer
|
||||
AVAudioFrameCount frameCount = decodeLen;
|
||||
AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:audioFormat frameCapacity:frameCount];
|
||||
buffer.frameLength = frameCount;
|
||||
|
||||
// 拷贝数据到 buffer
|
||||
for (int ch = 0; ch < audioConfig.channelCount; ch++) {
|
||||
float *dst = buffer.floatChannelData[ch];
|
||||
for (int i = 0; i < decodeLen; i++) {
|
||||
dst[i] = fbuf[i * audioConfig.channelCount + ch] * volume; // 非交错数据
|
||||
}
|
||||
}
|
||||
// 播放
|
||||
if(!audioSessionInterrupted) [audioPlayerNode scheduleBuffer:buffer completionHandler:nil];
|
||||
}
|
||||
|
||||
if (SDL_QueueAudio(audioDevice,
|
||||
audioBuffer,
|
||||
sizeof(float) * decodeLen * audioConfig.channelCount) < 0) {
|
||||
Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError());
|
||||
else{
|
||||
if(volume != 1.0){
|
||||
int totalSamples = decodeLen * audioConfig.channelCount;
|
||||
for (int i = 0; i < totalSamples; i++) {
|
||||
fbuf[i] *= volume;
|
||||
}
|
||||
}
|
||||
|
||||
while (SDL_GetQueuedAudioSize(audioDevice) / audioFrameSize > 10) {
|
||||
[NSThread sleepForTimeInterval:0.001f];
|
||||
}
|
||||
|
||||
if (SDL_QueueAudio(audioDevice,
|
||||
audioBuffer,
|
||||
sizeof(float) * decodeLen * audioConfig.channelCount) < 0) {
|
||||
Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,6 +535,9 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
|
||||
// won't be able to acquire it if LiStartConnection is in
|
||||
// progress.
|
||||
LiInterruptConnection();
|
||||
[audioPlayerNode stop];
|
||||
[audioEngine stop];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
|
||||
// We dispatch this async to get out because this can be invoked
|
||||
// on a thread inside common and we don't want to deadlock. It also avoids
|
||||
@@ -414,6 +549,27 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
|
||||
});
|
||||
}
|
||||
|
||||
- (void)handleAudioSessionInterruption:(NSNotification *)notification {
|
||||
NSDictionary *info = notification.userInfo;
|
||||
AVAudioSessionInterruptionType type =
|
||||
[info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
|
||||
|
||||
switch (type) {
|
||||
case AVAudioSessionInterruptionTypeBegan:
|
||||
audioSessionInterrupted = true;
|
||||
[audioPlayerNode stop];
|
||||
[audioEngine stop];
|
||||
break;
|
||||
case AVAudioSessionInterruptionTypeEnded:
|
||||
AudioEngineInit(audioConfig.sampleRate, audioConfig.channelCount);
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5*NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
audioSessionInterrupted = false;
|
||||
});
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks
|
||||
{
|
||||
self = [super init];
|
||||
@@ -461,14 +617,16 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
|
||||
_callbacks = callbacks;
|
||||
|
||||
LiInitializeStreamConfiguration(&_streamConfig);
|
||||
_streamConfig.colorRange = 1; // Full range
|
||||
_streamConfig.colorRange = config.fullColorRange ? 1 : 0;
|
||||
fullColorRange = config.fullColorRange;
|
||||
_streamConfig.width = config.width;
|
||||
_streamConfig.height = config.height;
|
||||
_streamConfig.fps = config.frameRate;
|
||||
_streamConfig.bitrate = config.bitRate;
|
||||
_streamConfig.supportedVideoFormats = config.supportedVideoFormats;
|
||||
_streamConfig.audioConfiguration = config.audioConfiguration;
|
||||
|
||||
_streamConfig.redirectMic = config.redirectMic && [MicHandler permissionGranted];
|
||||
[Connection setVolume:config.localVolume];
|
||||
// Since we require iOS 12 or above, we're guaranteed to be running
|
||||
// on a 64-bit device with ARMv8 crypto instructions, so we don't
|
||||
// need to check for that here.
|
||||
@@ -493,9 +651,10 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
|
||||
LiInitializeVideoCallbacks(&_drCallbacks);
|
||||
_drCallbacks.setup = DrDecoderSetup;
|
||||
_drCallbacks.cleanup = DrCleanup;
|
||||
// Use pull renderer for legacy frame pacing, direct submit for modern frame pacing
|
||||
// Use pull renderer for legacy and off frame pacing, direct submit for queue-based frame pacing
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
if ([[dataMan getSettings].framePacingMode integerValue] == FramePacingModeLegacy) {
|
||||
FramePacingMode framePacingMode = [[dataMan getSettings].framePacingMode integerValue];
|
||||
if (framePacingMode == FramePacingModeLegacy || framePacingMode == FramePacingModeOff) {
|
||||
_drCallbacks.capabilities = CAPABILITY_PULL_RENDERER |
|
||||
CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC |
|
||||
CAPABILITY_REFERENCE_FRAME_INVALIDATION_AV1;
|
||||
@@ -529,6 +688,11 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
|
||||
_clCallbacks.setMotionEventState = ClSetMotionEventState;
|
||||
_clCallbacks.setControllerLED = ClSetControllerLED;
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(handleAudioSessionInterruption:)
|
||||
name:AVAudioSessionInterruptionNotification
|
||||
object:nil];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
@property int serverCodecModeSupport;
|
||||
@property BOOL enableYUV444;
|
||||
@property BOOL enablePIP;
|
||||
@property BOOL fullColorRange;
|
||||
@property int width;
|
||||
@property int height;
|
||||
@property int frameRate;
|
||||
@@ -31,7 +32,9 @@
|
||||
@property int gamepadMask;
|
||||
@property BOOL optimizeGameSettings;
|
||||
@property BOOL playAudioOnPC;
|
||||
@property BOOL redirectMic;
|
||||
@property BOOL swapABXYButtons;
|
||||
@property BOOL buttonVisualFeedback;
|
||||
@property BOOL asyncNativeTouchPriority;
|
||||
@property int gyroMode;
|
||||
@property int emulatedControllerType;
|
||||
@@ -40,5 +43,6 @@
|
||||
@property BOOL multiController;
|
||||
@property NSData* serverCert;
|
||||
@property int localMousePointerMode;
|
||||
@property CGFloat localVolume;
|
||||
|
||||
@end
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
#import "StreamConfiguration.h"
|
||||
|
||||
@implementation StreamConfiguration
|
||||
@synthesize host, httpsPort, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, appName, optimizeGameSettings, playAudioOnPC, swapABXYButtons, gyroMode, emulatedControllerType, audioConfiguration, supportedVideoFormats, multiController, serverCert, rtspSessionUrl, serverCodecModeSupport, enableYUV444, enablePIP;
|
||||
@synthesize host, httpsPort, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, appName, optimizeGameSettings, playAudioOnPC, swapABXYButtons, buttonVisualFeedback, gyroMode, emulatedControllerType, audioConfiguration, supportedVideoFormats, multiController, serverCert, rtspSessionUrl, serverCodecModeSupport, enableYUV444, enablePIP, fullColorRange, localVolume;
|
||||
@end
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callback;
|
||||
|
||||
- (void) stopStream;
|
||||
- (void) setNeedRequeuing:(bool)needRequeuing;
|
||||
|
||||
- (NSString*) getStatsOverlayText: (uint16_t) overlayLevel;
|
||||
|
||||
|
||||
@@ -114,9 +114,16 @@
|
||||
});
|
||||
}
|
||||
|
||||
- (void) setNeedRequeuing:(bool)needRequeuing{
|
||||
if (_videoRenderer.queueSize==0) return;
|
||||
[_videoRenderer.frameQueue clear];
|
||||
_videoRenderer.needRequeuing = needRequeuing;
|
||||
}
|
||||
|
||||
- (void) stopStream
|
||||
{
|
||||
[_connection terminate];
|
||||
_callbacks = nil;
|
||||
}
|
||||
|
||||
- (BOOL) launchApp:(HttpManager*)hMan receiveSessionUrl:(NSString**)sessionUrl {
|
||||
@@ -169,7 +176,7 @@
|
||||
uint32_t rtt, variance;
|
||||
NSString* latencyString;
|
||||
if (LiGetEstimatedRttInfo(&rtt, &variance)) {
|
||||
latencyString = [LocalizationHelper localizedStringForKey:@"%u ms (variance: %u ms)", rtt, variance];
|
||||
latencyString = [LocalizationHelper localizedStringForKey:@"%3u ms (var: %3u ms)", rtt, variance];
|
||||
}
|
||||
else {
|
||||
latencyString = @"N/A";
|
||||
@@ -189,13 +196,13 @@
|
||||
|
||||
float interval = stats.endTime - stats.startTime;
|
||||
|
||||
// Check if we're using legacy pacing mode
|
||||
// Get frame pacing mode
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
BOOL isLegacyPacing = [[dataMan getSettings].framePacingMode integerValue] == FramePacingModeLegacy;
|
||||
FramePacingMode framePacingMode = [[dataMan getSettings].framePacingMode integerValue];
|
||||
|
||||
// Calculate FPS differently based on pacing mode
|
||||
float fps;
|
||||
if (isLegacyPacing) {
|
||||
if (framePacingMode == FramePacingModeLegacy || framePacingMode == FramePacingModeOff) {
|
||||
fps = stats.totalFrames / interval;
|
||||
} else {
|
||||
float scalePlotMetrics = stats.frameDropMetrics.nsamples > 0 ? ((float)stats.frameDropMetrics.nsamples / stats.totalFrames) : 1.0f;
|
||||
@@ -205,18 +212,25 @@
|
||||
double avgVideoMbps = [_connection getBwTracker].averageMbps;
|
||||
double peakVideoMbps = [_connection getBwTracker].peakMbps;
|
||||
|
||||
if(overlayLevel == 1) return [LocalizationHelper localizedStringForKey:@"FPS: %5.2f Network dropped frames: %.2f%% Network latency: %@",
|
||||
NSString* colorRange = _config.fullColorRange ? @"Full" : @"Limited";
|
||||
|
||||
if(overlayLevel == 1) return [LocalizationHelper localizedStringForKey:@"simplifiedOsdText",
|
||||
fps,
|
||||
stats.networkDroppedFrames / interval,
|
||||
avgVideoMbps,
|
||||
latencyString];
|
||||
else {
|
||||
if (isLegacyPacing) {
|
||||
if (framePacingMode == FramePacingModeLegacy || framePacingMode == FramePacingModeOff) {
|
||||
NSString* rendererWithPacing = stats.renderingBackendString;
|
||||
if ([stats.renderingBackendString isEqualToString:@"AVSampleBuffer"]) {
|
||||
rendererWithPacing = @"AVSampleBuffer (Legacy Pacing)";
|
||||
if (framePacingMode == FramePacingModeOff) {
|
||||
rendererWithPacing = @"AVSampleBuffer (No Pacing)";
|
||||
} else {
|
||||
rendererWithPacing = @"AVSampleBuffer (Legacy Pacing)";
|
||||
}
|
||||
}
|
||||
|
||||
return [LocalizationHelper localizedStringForKey:@"Video stream: %dx%d %.2f FPS (Codec: %@)\n"
|
||||
return [LocalizationHelper localizedStringForKey:@"Video stream: %dx%d %.2f FPS (Codec: %@, %@)\n"
|
||||
"Bitrate: %.1f Mbps, Peak: %.1f\n"
|
||||
"%@"
|
||||
"Renderer: %@\n"
|
||||
@@ -226,6 +240,7 @@
|
||||
_config.height,
|
||||
fps,
|
||||
[_connection getActiveCodecName],
|
||||
colorRange,
|
||||
avgVideoMbps, peakVideoMbps,
|
||||
hostProcessingString,
|
||||
rendererWithPacing,
|
||||
@@ -237,7 +252,7 @@
|
||||
rendererWithPacing = @"AVSampleBuffer (Queue Pacing)";
|
||||
}
|
||||
|
||||
return [LocalizationHelper localizedStringForKey:@"Video stream: %dx%d %.2f FPS (Codec: %@)\n"
|
||||
return [LocalizationHelper localizedStringForKey:@"Video stream: %dx%d %.2f FPS (Codec: %@, %@)\n"
|
||||
"Bitrate: %.1f Mbps, Peak: %.1f, Frames buffered: %.1f\n"
|
||||
"%@"
|
||||
"Renderer: %@\n"
|
||||
@@ -248,6 +263,7 @@
|
||||
_config.height,
|
||||
fps,
|
||||
[_connection getActiveCodecName],
|
||||
colorRange,
|
||||
avgVideoMbps, peakVideoMbps, stats.frameQueueMetrics.avg,
|
||||
hostProcessingString,
|
||||
rendererWithPacing,
|
||||
|
||||
@@ -18,12 +18,15 @@
|
||||
|
||||
@property (atomic, readonly) PlotMetrics decodeMetrics;
|
||||
@property (atomic, readonly) PlotMetrics frameQueueMetrics;
|
||||
@property (atomic, assign) bool needRequeuing;
|
||||
@property (nonatomic, strong) FrameQueue* frameQueue;
|
||||
@property (atomic, readonly) int32_t queueSize;
|
||||
|
||||
@property (nonatomic, strong, readonly) AVSampleBufferDisplayLayer *displayLayer;
|
||||
|
||||
- (id)initWithView:(UIView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio;
|
||||
|
||||
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate;
|
||||
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate fullRange:(BOOL)fullRange;
|
||||
- (void)renderFrame:(Frame *)frame atTime:(CMTime)targetTime;
|
||||
- (void)cleanup;
|
||||
- (void)setHdrMode:(BOOL)enabled;
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
// Created by Cameron Gutman on 10/18/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2026.2
|
||||
// Copyright © 2026 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
@import AVFoundation;
|
||||
@import VideoToolbox;
|
||||
@@ -36,12 +40,13 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
||||
@implementation VideoDecoderRenderer {
|
||||
dispatch_queue_t _sq, _vtq;
|
||||
StreamView* _view;
|
||||
id<ConnectionCallbacks> _callbacks;
|
||||
__weak id<ConnectionCallbacks> _callbacks;
|
||||
float _streamAspectRatio;
|
||||
|
||||
AVSampleBufferDisplayLayer* _displayLayer;
|
||||
int _videoFormat;
|
||||
int _frameRate;
|
||||
BOOL _fullRange;
|
||||
|
||||
NSMutableArray *_parameterSetBuffers;
|
||||
NSData *_masteringDisplayColorVolume;
|
||||
@@ -51,11 +56,15 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
||||
VTDecompressionSessionRef _decompressionSession;
|
||||
|
||||
CADisplayLink *_displayLink;
|
||||
FrameQueue *_frameQueue;
|
||||
NSInteger _maxRefreshRate;
|
||||
RenderingBackend _renderingBackend;
|
||||
|
||||
BOOL _useLegacyPacing;
|
||||
FramePacingMode _framePacingMode;
|
||||
bool _enableTimebase;
|
||||
bool _asyncFrameDequeue;
|
||||
|
||||
// CMTime playTime;
|
||||
// NSTimeInterval previousLinkTime;
|
||||
}
|
||||
|
||||
- (void)reinitializeDisplayLayer
|
||||
@@ -115,6 +124,8 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
||||
NSLog(@"initializing video decoder %f", CACurrentMediaTime());
|
||||
self = [super init];
|
||||
|
||||
appDidEnterBackgroundWithoutPip = false;
|
||||
|
||||
_sq = dispatch_queue_create("com.moonlight.VideoDecoderRenderer",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
|
||||
@@ -122,21 +133,28 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
||||
_vtq = dispatch_queue_create("com.moonlight.VideoDecoderRenderer.VTDecoder",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
|
||||
_view = view;
|
||||
_view = (StreamView*) view;
|
||||
_callbacks = callbacks;
|
||||
_streamAspectRatio = aspectRatio;
|
||||
_maxRefreshRate = [[UIScreen mainScreen] maximumFramesPerSecond];
|
||||
_parameterSetBuffers = [[NSMutableArray alloc] init];
|
||||
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
TemporarySettings* tempSettings = [dataMan getSettings];
|
||||
|
||||
_useLegacyPacing = [[dataMan getSettings].framePacingMode integerValue] == FramePacingModeLegacy;
|
||||
_framePacingMode = tempSettings.framePacingMode.integerValue;
|
||||
_asyncFrameDequeue = tempSettings.asyncFrameDequeue;
|
||||
NSLog(@"_asyncFrameDequeue %d", _asyncFrameDequeue);
|
||||
_enableTimebase = false;
|
||||
_queueSize = tempSettings.frameQueueSize.intValue;
|
||||
_needRequeuing = _queueSize>0;
|
||||
|
||||
_frameQueue = [FrameQueue sharedInstance];
|
||||
[_frameQueue start];
|
||||
[_frameQueue setHighWaterMark:(int)[[dataMan getSettings].frameQueueSize integerValue]];
|
||||
[_frameQueue setHighWaterMark:MAX(1, _queueSize)];
|
||||
|
||||
[self reinitializeDisplayLayer];
|
||||
// NSTimeInterval interval = 1.0/tempSettings.framerate.intValue;
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(reinitializeDisplayLayer)
|
||||
@@ -148,10 +166,11 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
||||
|
||||
# pragma mark DisplayLink vsync callback
|
||||
|
||||
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate
|
||||
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate fullRange:(BOOL)fullRange
|
||||
{
|
||||
self->_videoFormat = videoFormat;
|
||||
self->_frameRate = frameRate;
|
||||
self->_fullRange = fullRange;
|
||||
|
||||
// reset plot data in case we've already used it for a previous renderer
|
||||
[[ImGuiPlots sharedInstance] clearData];
|
||||
@@ -162,8 +181,8 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
||||
|
||||
// Choose the appropriate selector based on frame pacing mode
|
||||
SEL displayLinkSelector;
|
||||
if (_useLegacyPacing) {
|
||||
// Legacy frame pacing: use simple displayLinkCallback
|
||||
if (_framePacingMode == FramePacingModeLegacy || _framePacingMode == FramePacingModeOff) {
|
||||
// Legacy frame pacing or Off mode: use simple displayLinkCallback
|
||||
displayLinkSelector = @selector(displayLinkCallback:);
|
||||
} else {
|
||||
// PACING_MODE_VSYNC:
|
||||
@@ -220,10 +239,9 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
||||
#else
|
||||
NSNumber *pixelFormat = nil;
|
||||
if (self->_videoFormat & VIDEO_FORMAT_MASK_YUV444) {
|
||||
pixelFormat = @(kCVPixelFormatType_444YpCbCr10BiPlanarFullRange);
|
||||
}
|
||||
else {
|
||||
pixelFormat = @(kCVPixelFormatType_420YpCbCr10BiPlanarFullRange);
|
||||
pixelFormat = self->_fullRange ? @(kCVPixelFormatType_444YpCbCr10BiPlanarFullRange) : @(kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange);
|
||||
} else {
|
||||
pixelFormat = self->_fullRange ? @(kCVPixelFormatType_420YpCbCr10BiPlanarFullRange) : @(kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange);
|
||||
}
|
||||
NSMutableDictionary *destinationPixelBufferAttributes = [@{
|
||||
(id)kCVPixelBufferPixelFormatTypeKey : pixelFormat
|
||||
@@ -242,7 +260,7 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
||||
// Check for issues with the SampleBuffer, this should be much less likely since
|
||||
// AVSB is not actually decoding the frames anymore
|
||||
if (self->_displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
|
||||
Log(LOG_E, @"Display layer rendering failed: %@", _displayLayer.error);
|
||||
// Log(LOG_E, @"Display layer rendering failed: %@", _displayLayer.error);
|
||||
|
||||
// Recreate the display layer. We are already on the main thread,
|
||||
// so this is safe to do right here.
|
||||
@@ -258,66 +276,84 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
||||
|
||||
#pragma mark DisplayLink - Frame Pacing - Vsync with FrameQueue
|
||||
|
||||
/*
|
||||
// This frame pacing method was inspired by the behavior of moonlight-qt's Pacer class, although it has evolved
|
||||
// a few additional features. Incoming frames from Sunshine are asynchronously processed into a queue by the VideoRecv thread.
|
||||
// DisplayLink calls us every vsync we we try to present the most recent frame. We try to maintain a user-configurable buffer
|
||||
// of 1-5 frames. If the buffer is full, every other frame is dropped which just appears to the user as a lower framerate stream.
|
||||
// of 1-5 frames. If the buffer is full, every other frame is dropped which just appears to the user as a lower framerate stream. */
|
||||
- (void)renderModeAVSB:(CADisplayLink *)link {
|
||||
// NSTimeInterval current = link.targetTimestamp;
|
||||
// NSLog(@"link %f", link.duration);
|
||||
// previousLinkTime = current;
|
||||
|
||||
CFTimeInterval start = link.timestamp;
|
||||
CFTimeInterval deadline = link.targetTimestamp;
|
||||
CFTimeInterval targetTime = link.targetTimestamp;
|
||||
static CFTimeInterval lastTargetLocal = 0.0f;
|
||||
CFTimeInterval dl0 = CACurrentMediaTime();
|
||||
|
||||
/*
|
||||
static int lateCallbacks = 0;
|
||||
if (dl0 > deadline) {
|
||||
if (dl0 > nextFrameTime) {
|
||||
// we already missed it, count how often this happens
|
||||
lateCallbacks++;
|
||||
return;
|
||||
}
|
||||
}*/
|
||||
|
||||
[self checkDisplayLayer];
|
||||
|
||||
static CFTimeInterval avgOverhead = 0.004f; // averaged each callback
|
||||
CFTimeInterval waitFor = deadline - dl0 - avgOverhead;
|
||||
CFTimeInterval waitFor = targetTime - dl0;
|
||||
|
||||
/*
|
||||
if (waitFor < 0.001f) {
|
||||
NSLog(@"waitFor %f", waitFor);
|
||||
// waitFor = isIPhone ? waitFor : 0.0f;
|
||||
waitFor = 0.0f;
|
||||
}
|
||||
*/
|
||||
|
||||
// Get the next frame or wait if necessary. If no frame arrives the previous one will be redisplayed automatically.
|
||||
Frame *frame = [_frameQueue dequeueWithTimeout:waitFor];
|
||||
if (frame) {
|
||||
CFTimeInterval dl1 = CACurrentMediaTime();
|
||||
if(_needRequeuing ? _frameQueue.count>MAX(_queueSize-1,0) : true){
|
||||
// if(true){
|
||||
_needRequeuing = false;
|
||||
if(_asyncFrameDequeue){
|
||||
[_frameQueue dequeueWithTimeout:waitFor completion:^(Frame *frame) {
|
||||
if (frame) {
|
||||
// LogOnce(LOG_I, @"Frame pacing: using AVSampleBufferDisplayLayer target %f Hz with %d FPS stream", 1.0f / (deadline - start), self->_frameRate);
|
||||
[self renderFrame:frame atTime:CMTimeMakeWithSeconds(targetTime, NSEC_PER_SEC)];
|
||||
}
|
||||
}];
|
||||
}
|
||||
else{
|
||||
Frame *frame = [_frameQueue dequeueWithTimeoutSync:waitFor];
|
||||
if (frame) {
|
||||
// CFTimeInterval dl1 = CACurrentMediaTime();
|
||||
|
||||
LogOnce(LOG_I, @"Frame pacing: using AVSampleBufferDisplayLayer target %f Hz with %d FPS stream", 1.0f / (deadline - start), self->_frameRate);
|
||||
// LogOnce(LOG_I, @"Frame pacing: using AVSampleBufferDisplayLayer target %f Hz with %d FPS stream", 1.0f / (deadline - start), self->_frameRate);
|
||||
|
||||
// The system works best with properly timed video frames, which we time to the end of the next vsync period,
|
||||
// the earliest they can be displayed due to double-buffering.
|
||||
CFTimeInterval targetLocal = deadline + link.duration;
|
||||
// The system works best with properly timed video frames, which we time to the end of the next vsync period,
|
||||
// the earliest they can be displayed due to double-buffering.
|
||||
CFTimeInterval targetLocal = targetTime;
|
||||
|
||||
[self renderFrame:frame atTime:CMTimeMakeWithSeconds(targetLocal, NSEC_PER_SEC)];
|
||||
[self renderFrame:frame atTime:CMTimeMakeWithSeconds(targetLocal, NSEC_PER_SEC)];
|
||||
|
||||
#ifdef DISPLAYLINK_VERBOSE
|
||||
Log(LOG_I, @"[%.3f] rendering frame %d, waitFor %.3f ms, overhead %.3f ms, lateCallbacks %d, queue size %d",
|
||||
deadline, frame.frameNumber, waitFor * 1000.0, avgOverhead * 1000.0, lateCallbacks, [_frameQueue count]);
|
||||
Log(LOG_I, @"[%.3f] rendering frame %d, waitFor %.3f ms, overhead %.3f ms, lateCallbacks %d, queue size %d",
|
||||
deadline, frame.frameNumber, waitFor * 1000.0, avgOverhead * 1000.0, lateCallbacks, [_frameQueue count]);
|
||||
#endif
|
||||
|
||||
// Update metrics
|
||||
if (lastTargetLocal != 0) {
|
||||
CFTimeInterval frametime = targetLocal - lastTargetLocal;
|
||||
if (frametime > deadline - start + 0.0005f) {
|
||||
// we missed a callback
|
||||
// Log(LOG_W, @"*** slow frametime %.3f ms", frametime * 1000.0);
|
||||
}
|
||||
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) {
|
||||
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:frametime * 1000.0];
|
||||
// Update metrics
|
||||
if (lastTargetLocal != 0) {
|
||||
CFTimeInterval frametime = targetLocal - lastTargetLocal;
|
||||
if (frametime > targetTime - start + 0.0005f) {
|
||||
// we missed a callback
|
||||
// Log(LOG_W, @"*** slow frametime %.3f ms", frametime * 1000.0);
|
||||
}
|
||||
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) {
|
||||
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:frametime * 1000.0];
|
||||
}
|
||||
}
|
||||
lastTargetLocal = targetLocal;
|
||||
}
|
||||
}
|
||||
lastTargetLocal = targetLocal;
|
||||
|
||||
// weighted moving average of how much time displayLink needs after dequeuing a frame.
|
||||
// This is used to avoid overshooting a vsync by waiting too long.
|
||||
const double alpha = 0.1f;
|
||||
avgOverhead = ((CACurrentMediaTime() - dl1) * alpha) + (avgOverhead * (1.0 - alpha));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,17 +368,20 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
||||
while (LiPollNextVideoFrame(&handle, &du)) {
|
||||
LiCompleteVideoFrame(handle, DrSubmitDecodeUnit(du));
|
||||
|
||||
// Calculate the actual display refresh rate
|
||||
double displayRefreshRate = 1 / (_displayLink.targetTimestamp - _displayLink.timestamp);
|
||||
// Skip frame pacing logic if frame pacing is off
|
||||
if (_framePacingMode != FramePacingModeOff) {
|
||||
// Calculate the actual display refresh rate
|
||||
double displayRefreshRate = 1 / (_displayLink.targetTimestamp - _displayLink.timestamp);
|
||||
|
||||
// Only pace frames if the display refresh rate is >= 90% of our stream frame rate.
|
||||
// Battery saver, accessibility settings, or device thermals can cause the actual
|
||||
// refresh rate of the display to drop below the physical maximum.
|
||||
if (displayRefreshRate >= _frameRate * 0.9f) {
|
||||
// Keep one pending frame to smooth out gaps due to
|
||||
// network jitter at the cost of 1 frame of latency
|
||||
if (LiGetPendingVideoFrames() == 1) {
|
||||
break;
|
||||
// Only pace frames if the display refresh rate is >= 90% of our stream frame rate.
|
||||
// Battery saver, accessibility settings, or device thermals can cause the actual
|
||||
// refresh rate of the display to drop below the physical maximum.
|
||||
if (displayRefreshRate >= _frameRate * 0.9f) {
|
||||
// Keep one pending frame to smooth out gaps due to
|
||||
// network jitter at the cost of 1 frame of latency
|
||||
if (LiGetPendingVideoFrames() == 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,7 +391,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
||||
- (void)renderFrame:(Frame *)frame atTime:(CMTime)targetTime {
|
||||
CMSampleBufferSetOutputPresentationTimeStamp(frame.sampleBuffer, targetTime);
|
||||
|
||||
if ([self->_displayLayer controlTimebase] == NULL) {
|
||||
if (_enableTimebase && [self->_displayLayer controlTimebase] == NULL) {
|
||||
// On first frame, set timebase to the initial presentation time.
|
||||
// This will let us present frames using the local clock (vsync pacing) or
|
||||
// the pts timestamps from the host.
|
||||
@@ -371,7 +410,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
||||
Log(LOG_I, @"Setting timebase for stream to %d / %d", pts.value, pts.timescale);
|
||||
}
|
||||
|
||||
[self->_displayLayer enqueueSampleBuffer:frame.sampleBuffer];
|
||||
if(appDidEnterBackgroundWithoutPip) [self->_displayLayer flush];
|
||||
else [self->_displayLayer enqueueSampleBuffer:frame.sampleBuffer];
|
||||
|
||||
#ifdef DISPLAYLINK_VERBOSE
|
||||
// Some OS-level metrics I'm not sure what to do with
|
||||
@@ -403,21 +443,22 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
[_displayLink invalidate];
|
||||
}
|
||||
- (void)cleanup
|
||||
{
|
||||
[_frameQueue stop];
|
||||
|
||||
if (_renderingBackend == RENDER_AVSB) {
|
||||
[_displayLink invalidate];
|
||||
}
|
||||
- (void)cleanup{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self->_frameQueue stop];
|
||||
|
||||
@synchronized(self) {
|
||||
if (_decompressionSession != NULL) {
|
||||
VTDecompressionSessionInvalidate(_decompressionSession);
|
||||
CFRelease(_decompressionSession);
|
||||
_decompressionSession = nil;
|
||||
if (self->_renderingBackend == RENDER_AVSB) {
|
||||
[self->_displayLink invalidate];
|
||||
}
|
||||
}
|
||||
@synchronized(self) {
|
||||
if (self->_decompressionSession != NULL) {
|
||||
VTDecompressionSessionInvalidate(self->_decompressionSession);
|
||||
CFRelease(self->_decompressionSession);
|
||||
self->_decompressionSession = nil;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#define NALU_START_PREFIX_SIZE 3
|
||||
@@ -856,7 +897,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
||||
|
||||
CMSampleBufferRef sampleBuffer;
|
||||
CMTime presentationTimeStamp;
|
||||
if (_useLegacyPacing) {
|
||||
if (_framePacingMode == FramePacingModeLegacy || _framePacingMode == FramePacingModeOff) {
|
||||
presentationTimeStamp = CMTimeMake(du->presentationTimeUs / 1000, 1000);
|
||||
} else {
|
||||
presentationTimeStamp = CMTimeMake((int64_t)du->rtpTimestamp, 90000);
|
||||
@@ -881,9 +922,10 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
|
||||
if (_useLegacyPacing) {
|
||||
if (_framePacingMode == FramePacingModeLegacy || _framePacingMode == FramePacingModeOff) {
|
||||
// Enqueue the next frame
|
||||
[self->_displayLayer enqueueSampleBuffer:sampleBuffer];
|
||||
if(appDidEnterBackgroundWithoutPip) [self->_displayLayer flush];
|
||||
else [self->_displayLayer enqueueSampleBuffer:sampleBuffer];
|
||||
|
||||
if (du->frameType == FRAME_TYPE_IDR) {
|
||||
// Ensure the layer is visible now
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
//
|
||||
// ThemeManager.h
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025.5.25.
|
||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface ThemeManager:NSObject
|
||||
|
||||
@property (class, nonatomic, assign) UIUserInterfaceStyle userInterfaceStyle;
|
||||
+ (UIColor *)appPrimaryColor;
|
||||
+ (UIColor *)appSecondaryColor;
|
||||
+ (UIColor *)appPrimaryColorWithAlpha;
|
||||
+ (UIColor *)widgetBackgroundColor;
|
||||
+ (UIColor *)appBackgroundColor;
|
||||
+ (UIColor *)separatorColor;
|
||||
+ (UIColor *)textColor;
|
||||
+ (UIColor *)textColorGray;
|
||||
+ (UIColor *)lowProfileGray;
|
||||
+ (UIColor *)textTintColorWithAlpha;
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
//
|
||||
// ThemeManager.h
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025.5.25.
|
||||
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ThemeManager.h"
|
||||
|
||||
@implementation ThemeManager
|
||||
|
||||
static UIUserInterfaceStyle _privateUserInterfaceStyle = UIUserInterfaceStyleUnspecified;
|
||||
static UIUserInterfaceStyle _userInterfaceStyle;
|
||||
|
||||
+ (UIColor *)getUIStyle{
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UITraitCollection *traitCollection = [UIScreen mainScreen].traitCollection;
|
||||
if(_privateUserInterfaceStyle == UIUserInterfaceStyleUnspecified) {
|
||||
_userInterfaceStyle = traitCollection.userInterfaceStyle;
|
||||
}
|
||||
else _userInterfaceStyle = _privateUserInterfaceStyle;
|
||||
return [UIColor clearColor];
|
||||
} else {
|
||||
return [UIColor clearColor];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+ (UIUserInterfaceStyle)userInterfaceStyle {
|
||||
[ThemeManager getUIStyle];
|
||||
return _userInterfaceStyle;
|
||||
}
|
||||
|
||||
+ (void)setUserInterfaceStyle:(UIUserInterfaceStyle)style {
|
||||
_privateUserInterfaceStyle = style;
|
||||
[ThemeManager getUIStyle];
|
||||
}
|
||||
|
||||
+ (UIColor *)appBackgroundColor {
|
||||
[ThemeManager getUIStyle];
|
||||
switch (self.userInterfaceStyle) {
|
||||
case UIUserInterfaceStyleLight:
|
||||
return [UIColor colorWithRed:242.0/255.0 green:242.0/255.0 blue:247.0/255.0 alpha:1.0];
|
||||
break;
|
||||
case UIUserInterfaceStyleDark:
|
||||
default:
|
||||
return [UIColor colorWithRed:28.0/255.0 green:28.0/255.0 blue:30.0/255.0 alpha:1.0];
|
||||
//return [UIColor blackColor];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+ (UIColor *)widgetBackgroundColor {
|
||||
[ThemeManager getUIStyle];
|
||||
switch (self.userInterfaceStyle) {
|
||||
case UIUserInterfaceStyleLight:
|
||||
return [UIColor whiteColor];
|
||||
break;
|
||||
case UIUserInterfaceStyleDark:
|
||||
default:
|
||||
return [UIColor colorWithRed:44.0/255.0 green:44.0/255.0 blue:46.0/255.0 alpha:1.0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+ (UIColor *)separatorColor {
|
||||
[ThemeManager getUIStyle];
|
||||
switch (self.userInterfaceStyle) {
|
||||
case UIUserInterfaceStyleLight:
|
||||
return [UIColor colorWithWhite:0.1 alpha:0.28];
|
||||
break;
|
||||
case UIUserInterfaceStyleDark:
|
||||
default:
|
||||
return [UIColor colorWithWhite:0.28 alpha:1.0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+ (UIColor *)textColor {
|
||||
[ThemeManager getUIStyle];
|
||||
switch (self.userInterfaceStyle) {
|
||||
case UIUserInterfaceStyleLight:
|
||||
return [UIColor blackColor];
|
||||
break;
|
||||
case UIUserInterfaceStyleDark:
|
||||
default:
|
||||
return [UIColor whiteColor];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+ (UIColor *)appPrimaryColor {
|
||||
return [UIColor colorWithRed:0.0 green:0.48 blue:1.0 alpha:1.0]; // #0A84FF
|
||||
}
|
||||
|
||||
+ (UIColor *)appSecondaryColor {
|
||||
// return [UIColor colorWithRed:0.0 green:0.319 blue:0.64 alpha:1.0]; // 可换其他常用色
|
||||
UIColor* originalColor = [ThemeManager appPrimaryColor];
|
||||
|
||||
UIColor *grayColor = [UIColor grayColor];
|
||||
CGFloat mixRatio = 0.3; // 混合比例,0.0 到 1.0
|
||||
|
||||
CGFloat r1, g1, b1, a1;
|
||||
CGFloat r2, g2, b2, a2;
|
||||
|
||||
[originalColor getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
|
||||
[grayColor getRed:&r2 green:&g2 blue:&b2 alpha:&a2];
|
||||
|
||||
CGFloat r = r1 * (1 - mixRatio) + r2 * mixRatio;
|
||||
CGFloat g = g1 * (1 - mixRatio) + g2 * mixRatio;
|
||||
CGFloat b = b1 * (1 - mixRatio) + b2 * mixRatio;
|
||||
CGFloat a = a1 * (1 - mixRatio) + a2 * mixRatio;
|
||||
|
||||
UIColor *tonedColor = [UIColor colorWithRed:r green:g blue:b alpha:a];
|
||||
|
||||
return tonedColor; // 可换其他常用色
|
||||
}
|
||||
|
||||
+ (UIColor *)appPrimaryColorWithAlpha {
|
||||
[ThemeManager getUIStyle];
|
||||
switch (self.userInterfaceStyle) {
|
||||
case UIUserInterfaceStyleLight:
|
||||
return [[ThemeManager appPrimaryColor] colorWithAlphaComponent:0.24]; // #0A84FF
|
||||
break;
|
||||
case UIUserInterfaceStyleDark:
|
||||
default:
|
||||
return [[ThemeManager appPrimaryColor] colorWithAlphaComponent:0.24];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+ (UIColor *)textTintColorWithAlpha{
|
||||
return [[ThemeManager appPrimaryColor] colorWithAlphaComponent:0.24];
|
||||
}
|
||||
|
||||
+ (UIColor *)textColorGray{
|
||||
return [UIColor colorWithRed:0.55 green:0.55 blue:0.6 alpha:0.95];
|
||||
}
|
||||
|
||||
+ (UIColor *)lowProfileGray{
|
||||
return [UIColor colorWithRed:0.65 green:0.65 blue:0.65 alpha:0.4];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,328 @@
|
||||
//
|
||||
// ThemeManager.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2026/3/6.
|
||||
// Copyright © 2026 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
import UIKit
|
||||
|
||||
@objcMembers
|
||||
class ThemeManager: NSObject {
|
||||
|
||||
static let ThemeDidChangeNotification = "ThemeDidChangeNotification"
|
||||
|
||||
private static var _privateUserInterfaceStyle: UIUserInterfaceStyle = .unspecified
|
||||
private static var _userInterfaceStyle: UIUserInterfaceStyle = .unspecified
|
||||
|
||||
@objc class func setPublicUIStyle() -> UIColor {
|
||||
if #available(iOS 13.0, *) {
|
||||
let traitCollection = UIScreen.main.traitCollection
|
||||
if _privateUserInterfaceStyle == .unspecified {
|
||||
_userInterfaceStyle = traitCollection.userInterfaceStyle
|
||||
} else {
|
||||
_userInterfaceStyle = _privateUserInterfaceStyle
|
||||
}
|
||||
}
|
||||
return UIColor.clear
|
||||
}
|
||||
|
||||
@objc class func userInterfaceStyle() -> UIUserInterfaceStyle {
|
||||
_ = setPublicUIStyle()
|
||||
return _userInterfaceStyle
|
||||
}
|
||||
|
||||
@objc class func setUserInterfaceStyle(_ style: UIUserInterfaceStyle) {
|
||||
_privateUserInterfaceStyle = style
|
||||
|
||||
if _userInterfaceStyle == style {
|
||||
return
|
||||
}
|
||||
|
||||
_ = setPublicUIStyle()
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.Name(ThemeDidChangeNotification),
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@objc static var menuBackgroundColor: UIColor {
|
||||
_ = setPublicUIStyle()
|
||||
|
||||
switch userInterfaceStyle() {
|
||||
case .light:
|
||||
return UIColor(
|
||||
red: 242.0/255.0,
|
||||
green: 242.0/255.0,
|
||||
blue: 247.0/255.0,
|
||||
alpha: 1
|
||||
)
|
||||
|
||||
default:
|
||||
if #available(iOS 13.0, *) {
|
||||
let darkTraits = UITraitCollection(userInterfaceStyle: .dark)
|
||||
return UIColor.secondarySystemBackground.resolvedColor(with: darkTraits)
|
||||
} else {
|
||||
return UIColor(
|
||||
red: 28.0/255.0,
|
||||
green: 28.0/255.0,
|
||||
blue: 30.0/255.0,
|
||||
alpha: 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc static var hostViewBackgroundColor: UIColor {
|
||||
_ = setPublicUIStyle()
|
||||
|
||||
switch userInterfaceStyle() {
|
||||
case .light:
|
||||
return UIColor(
|
||||
red: 242.0/255.0,
|
||||
green: 242.0/255.0,
|
||||
blue: 247.0/255.0,
|
||||
alpha: 1
|
||||
)
|
||||
|
||||
default:
|
||||
if #available(iOS 13.0, *) {
|
||||
let darkTraits = UITraitCollection(userInterfaceStyle: .dark)
|
||||
return UIColor.systemGroupedBackground.resolvedColor(with: darkTraits)
|
||||
} else {
|
||||
return UIColor.black
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc static var offlineHostIconBackgroundColor: UIColor {
|
||||
_ = setPublicUIStyle()
|
||||
|
||||
switch userInterfaceStyle() {
|
||||
case .light:
|
||||
return UIColor(
|
||||
red: 242.0/255.0,
|
||||
green: 242.0/255.0,
|
||||
blue: 247.0/255.0,
|
||||
alpha: 1
|
||||
)
|
||||
|
||||
default:
|
||||
return UIColor(
|
||||
red: 17.0/255.0,
|
||||
green: 17.0/255.0,
|
||||
blue: 19.0/255.0,
|
||||
alpha: 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc static var widgetBackgroundColor: UIColor {
|
||||
_ = setPublicUIStyle()
|
||||
|
||||
switch userInterfaceStyle() {
|
||||
case .light:
|
||||
return UIColor.white
|
||||
default:
|
||||
if #available(iOS 13.0, *) {
|
||||
// return UIColor.secondarySystemBackground
|
||||
let darkTraits = UITraitCollection(userInterfaceStyle: .dark)
|
||||
return UIColor.secondarySystemGroupedBackground.resolvedColor(with: darkTraits)
|
||||
} else {
|
||||
return UIColor(
|
||||
red: 44.0/255.0,
|
||||
green: 44.0/255.0,
|
||||
blue: 46.0/255.0,
|
||||
alpha: 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc static var separatorColor: UIColor {
|
||||
_ = setPublicUIStyle()
|
||||
switch userInterfaceStyle() {
|
||||
case .light:
|
||||
if #available(iOS 13.0, *) {
|
||||
let lightTraits = UITraitCollection(userInterfaceStyle: .light)
|
||||
return GenericUtils.liquidGlassEnabled ? legacySepratorColor : UIColor.separator.resolvedColor(with: lightTraits)
|
||||
} else {
|
||||
return UIColor(
|
||||
red: 214.0/255.0,
|
||||
green: 214.0/255.0,
|
||||
blue: 218.0/255.0,
|
||||
alpha: 1
|
||||
)
|
||||
}
|
||||
default:
|
||||
if #available(iOS 13.0, *) {
|
||||
let darkTraits = UITraitCollection(userInterfaceStyle: .dark)
|
||||
return GenericUtils.liquidGlassEnabled ? legacySepratorColor : UIColor.separator.resolvedColor(with: darkTraits)
|
||||
} else {return UIColor(white: 0.28, alpha: 1)}
|
||||
}
|
||||
}
|
||||
|
||||
@objc static var legacySepratorColor: UIColor {
|
||||
_ = setPublicUIStyle()
|
||||
switch userInterfaceStyle() {
|
||||
case .light:
|
||||
return UIColor(
|
||||
red: 0.23529411764705882,
|
||||
green: 0.23529411764705882,
|
||||
blue: 0.2627450980392157,
|
||||
alpha: 0.29
|
||||
)
|
||||
default:
|
||||
return UIColor(
|
||||
red: 0.32941176470588235,
|
||||
green: 0.32941176470588235,
|
||||
blue: 0.34509803921568627,
|
||||
alpha: 0.6
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc static var hostCardSeparatorColor: UIColor {
|
||||
_ = setPublicUIStyle()
|
||||
switch userInterfaceStyle() {
|
||||
case .light:
|
||||
if #available(iOS 13.0, *) {
|
||||
let lightTraits = UITraitCollection(userInterfaceStyle: .light)
|
||||
return UIColor.separator.resolvedColor(with: lightTraits)
|
||||
} else {
|
||||
return UIColor(
|
||||
red: 214.0/255.0,
|
||||
green: 214.0/255.0,
|
||||
blue: 218.0/255.0,
|
||||
alpha: 1
|
||||
)
|
||||
}
|
||||
default:
|
||||
if #available(iOS 13.0, *) {
|
||||
let darkTraits = UITraitCollection(userInterfaceStyle: .dark)
|
||||
return UIColor.separator.resolvedColor(with: darkTraits)
|
||||
} else {return UIColor(white: 0.28, alpha: 1)}
|
||||
}
|
||||
}
|
||||
|
||||
@objc static var textColor: UIColor {
|
||||
_ = setPublicUIStyle()
|
||||
|
||||
switch userInterfaceStyle() {
|
||||
case .light:
|
||||
return UIColor.black
|
||||
default:
|
||||
return UIColor.white
|
||||
}
|
||||
}
|
||||
|
||||
@objc static var sectionLabelTextColor: UIColor {
|
||||
_ = setPublicUIStyle()
|
||||
|
||||
switch userInterfaceStyle() {
|
||||
case .light:
|
||||
if #available(iOS 13.0, *) {
|
||||
let traits = UITraitCollection(userInterfaceStyle: .light)
|
||||
return UIColor.label.resolvedColor(with: traits)
|
||||
} else {
|
||||
return UIColor.black
|
||||
}
|
||||
default:
|
||||
if #available(iOS 13.0, *) {
|
||||
let traits = UITraitCollection(userInterfaceStyle: .dark)
|
||||
return UIColor.label.resolvedColor(with: traits)
|
||||
} else {
|
||||
return UIColor.white
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc static var appPrimaryColor: UIColor {
|
||||
return UIColor(
|
||||
red: 0.0,
|
||||
green: 0.48,
|
||||
blue: 1.0,
|
||||
alpha: 1.0
|
||||
) // #0A84FF
|
||||
}
|
||||
|
||||
@objc static var appSecondaryColor: UIColor {
|
||||
|
||||
let originalColor = appPrimaryColor
|
||||
let grayColor = UIColor.gray
|
||||
let mixRatio: CGFloat = 0.3
|
||||
|
||||
var r1: CGFloat = 0
|
||||
var g1: CGFloat = 0
|
||||
var b1: CGFloat = 0
|
||||
var a1: CGFloat = 0
|
||||
|
||||
var r2: CGFloat = 0
|
||||
var g2: CGFloat = 0
|
||||
var b2: CGFloat = 0
|
||||
var a2: CGFloat = 0
|
||||
|
||||
originalColor.getRed(&r1, green: &g1, blue: &b1, alpha: &a1)
|
||||
grayColor.getRed(&r2, green: &g2, blue: &b2, alpha: &a2)
|
||||
|
||||
let r = r1 * (1 - mixRatio) + r2 * mixRatio
|
||||
let g = g1 * (1 - mixRatio) + g2 * mixRatio
|
||||
let b = b1 * (1 - mixRatio) + b2 * mixRatio
|
||||
let a = a1 * (1 - mixRatio) + a2 * mixRatio
|
||||
|
||||
return UIColor(red: r, green: g, blue: b, alpha: a)
|
||||
}
|
||||
|
||||
@objc static var appPrimaryColorWithAlpha: UIColor {
|
||||
return appPrimaryColor.withAlphaComponent(0.24)
|
||||
}
|
||||
|
||||
@objc static var textTintColorWithAlpha: UIColor {
|
||||
return appPrimaryColor.withAlphaComponent(0.24)
|
||||
}
|
||||
|
||||
@objc static var textColorGray: UIColor {
|
||||
return UIColor(
|
||||
red: 0.55,
|
||||
green: 0.55,
|
||||
blue: 0.6,
|
||||
alpha: 0.95
|
||||
)
|
||||
}
|
||||
|
||||
@objc static var lowProfileGray: UIColor {
|
||||
return UIColor(
|
||||
red: 0.65,
|
||||
green: 0.65,
|
||||
blue: 0.65,
|
||||
alpha: 0.4
|
||||
)
|
||||
}
|
||||
|
||||
@available(iOS 26.0, *)
|
||||
@objc static var liquidGlassSwitchOffTint: UIColor {
|
||||
switch userInterfaceStyle() {
|
||||
case .light:
|
||||
return UIColor.systemFill
|
||||
default:
|
||||
return UIColor.clear
|
||||
}
|
||||
}
|
||||
|
||||
@objc static var liquidGlassSliderMaxTrackTint: UIColor {
|
||||
if #available(iOS 13.0, *) {
|
||||
switch userInterfaceStyle() {
|
||||
case .light:
|
||||
return UIColor.tertiarySystemFill
|
||||
default:
|
||||
return UIColor.tertiarySystemFill
|
||||
}
|
||||
}
|
||||
else {
|
||||
return UIColor.black
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
#import "UIAppView.h"
|
||||
#import "AppAssetManager.h"
|
||||
#import "ThemeManager.h"
|
||||
#import "VoidLink-Swift.h"
|
||||
|
||||
static const float REFRESH_CYCLE = 1.0f;
|
||||
|
||||
@@ -178,7 +178,7 @@ static UIImage* noImage;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:23];
|
||||
UIImageView* playIcon = [[UIImageView alloc] initWithImage:[[UIImage systemImageNamed:@"play.circle.fill" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
|
||||
//playIcon.tintColor = [[ThemeManager widgetBackgroundColor] colorWithAlphaComponent:0.85];
|
||||
//playIcon.tintColor = [ThemeManager.widgetBackgroundColor colorWithAlphaComponent:0.85];
|
||||
playIcon.tintColor = [[UIColor blackColor] colorWithAlphaComponent:0.55];
|
||||
|
||||
_appOverlay = playIcon;
|
||||
@@ -202,7 +202,7 @@ static UIImage* noImage;
|
||||
UIImage* appIconImage = [[UIImage imageNamed:@"icon-pc-app"] imageWithConfiguration:config];
|
||||
UIImageView* appIcon = [[UIImageView alloc] initWithImage:[appIconImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
|
||||
|
||||
//layIcon.tintColor = [[ThemeManager widgetBackgroundColor] colorWithAlphaComponent:0.85];
|
||||
//layIcon.tintColor = [ThemeManager.widgetBackgroundColor colorWithAlphaComponent:0.85];
|
||||
appIcon.tintColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
|
||||
|
||||
_appOverlay = appIcon;
|
||||
|
||||
@@ -13,6 +13,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property (nonatomic) int highWaterMark;
|
||||
@property (nonatomic, readonly) int maxCapacity;
|
||||
@property (atomic) BOOL paused;
|
||||
- (void)dequeueWithTimeout:(CFTimeInterval)timeout
|
||||
completion:(void (^)(Frame *frame))completion;
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
@@ -23,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (int)enqueue:(Frame *)frame;
|
||||
- (int)enqueue:(Frame *)frame withSlackSize:(int)slack;
|
||||
- (nullable Frame *)dequeue;
|
||||
- (nullable Frame *)dequeueWithTimeout:(CFTimeInterval)timeout;
|
||||
- (nullable Frame *)dequeueWithTimeoutSync:(CFTimeInterval)timeout;
|
||||
- (CFTimeInterval)estimatedFramerate;
|
||||
- (int)currentSoftCap;
|
||||
- (void)waitForEnqueue;
|
||||
|
||||
@@ -251,7 +251,19 @@
|
||||
return frame;
|
||||
}
|
||||
|
||||
- (Frame *)dequeueWithTimeout:(CFTimeInterval)timeout {
|
||||
- (void)dequeueWithTimeout:(CFTimeInterval)timeout
|
||||
completion:(void (^)(Frame *frame))completion {
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
Frame *frame = [self dequeueWithTimeoutSync:timeout];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (completion) {
|
||||
completion(frame);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (Frame *)dequeueWithTimeoutSync:(CFTimeInterval)timeout {
|
||||
CFTimeInterval start = CACurrentMediaTime();
|
||||
CFTimeInterval deadline = start + timeout;
|
||||
int round = 0;
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// GenericUtils.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2026/2/26.
|
||||
// Copyright © 2026 True砖家@Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc public class GenericUtils: NSObject {
|
||||
|
||||
@objc static func needUpdateDefaultSettings() -> Bool {
|
||||
// let key = "needUpdateDefaultSettings20260226-1"
|
||||
let key = "needUpdateDefaultSettings20260226-2"
|
||||
guard !UserDefaults.standard.bool(forKey: key) else {
|
||||
return false
|
||||
}
|
||||
UserDefaults.standard.set(true, forKey: key)
|
||||
return true
|
||||
}
|
||||
|
||||
@objc static func isEnableOswForNativeTouchSwitchFirstFlipping() -> Bool {
|
||||
let key = "enableOswForNativeTouchSwitchFlipped"
|
||||
guard !UserDefaults.standard.bool(forKey: key) else {
|
||||
return false
|
||||
}
|
||||
UserDefaults.standard.set(true, forKey: key)
|
||||
return true
|
||||
}
|
||||
|
||||
@objc static func isFirstLaunchPressureCurveTool() -> Bool {
|
||||
let key = "hasLaunchedPressureCurveTool"
|
||||
let defaults = UserDefaults.standard
|
||||
let launchedBefore = defaults.bool(forKey: key)
|
||||
|
||||
if !launchedBefore {
|
||||
defaults.set(true, forKey: key)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@objc static func isIPhone() -> Bool {
|
||||
return UIDevice.current.userInterfaceIdiom == .phone
|
||||
}
|
||||
|
||||
@objc static func isIPad() -> Bool {
|
||||
return UIDevice.current.userInterfaceIdiom == .pad
|
||||
}
|
||||
|
||||
@objc static let liquidGlassEnabled: Bool = {
|
||||
if #available(iOS 26.0, tvOS 26.0, *) {
|
||||
let useLegacyUI = Bundle.main.object(forInfoDictionaryKey: "UIDesignRequiresCompatibility") as? Bool
|
||||
return useLegacyUI != true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}()
|
||||
|
||||
@objc static let menuSeparatorWidth: CGFloat = 0.7
|
||||
@objc static let menuSectionSeparatorWidth: CGFloat = 0.7
|
||||
|
||||
@objc static var legacyToolbarHeight: CGFloat {
|
||||
return 44
|
||||
}
|
||||
|
||||
@objc static var hostViewNavigationBarHeight: CGFloat {
|
||||
switch UIDevice.current.userInterfaceIdiom {
|
||||
case .phone:
|
||||
return liquidGlassEnabled ? 54 : 44
|
||||
case .pad:
|
||||
return liquidGlassEnabled ? 54 : 50
|
||||
default:
|
||||
return liquidGlassEnabled ? 54 : 50
|
||||
}
|
||||
}
|
||||
|
||||
@objc static var settingsMenuNavigationBarHeight: CGFloat {
|
||||
// return isIPhone() ? 44 : hostViewNavigationBarHeight
|
||||
switch UIDevice.current.userInterfaceIdiom {
|
||||
case .phone:
|
||||
return liquidGlassEnabled ? hostViewNavigationBarHeight+5 : hostViewNavigationBarHeight
|
||||
case .pad:
|
||||
return liquidGlassEnabled ? hostViewNavigationBarHeight+9 : hostViewNavigationBarHeight
|
||||
default:
|
||||
return liquidGlassEnabled ? hostViewNavigationBarHeight+9 : hostViewNavigationBarHeight
|
||||
}
|
||||
}
|
||||
|
||||
@objc static var dockedNavBarTopAnchorOffset: CGFloat {
|
||||
return liquidGlassEnabled ? 10 : 0
|
||||
}
|
||||
|
||||
@available(iOS 26.0, *)
|
||||
@objc static func applyOffTintColor(_ view: UIView) {
|
||||
let name = String(describing: type(of: view))
|
||||
if name.contains("UISwitchModernVisualElement") {
|
||||
view.backgroundColor = ThemeManager.liquidGlassSwitchOffTint
|
||||
view.layer.cornerRadius = view.bounds.height/2
|
||||
view.clipsToBounds = true
|
||||
}
|
||||
for sub in view.subviews {
|
||||
applyOffTintColor(sub)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
//
|
||||
// GraphicUtils.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2025/12/24.
|
||||
// Copyright © 2025 True砖家@Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
import SVGKit
|
||||
|
||||
@objc public class GraphicUtils: NSObject {
|
||||
@objc public static func makeSVGLayer(
|
||||
from file: String,
|
||||
in container: CALayer,
|
||||
at normalizedPosition: CGPoint = .zero,
|
||||
targetSize: CGSize
|
||||
) -> CALayer {
|
||||
|
||||
guard let url = Bundle.main.url(forResource: file, withExtension: "svg"),
|
||||
let data = try? Data(contentsOf: url) else {
|
||||
return CALayer()
|
||||
}
|
||||
|
||||
guard let svg = SVGKImage(data: data) else {
|
||||
fatalError("Failed to load SVG \(file)")
|
||||
}
|
||||
|
||||
return _makeSVGLayer(
|
||||
from: svg,
|
||||
in: container,
|
||||
at: normalizedPosition,
|
||||
targetSize: targetSize
|
||||
)
|
||||
}
|
||||
|
||||
public static func makeCenteredSVGLayer(
|
||||
from svg: SVGKImage,
|
||||
in container: CALayer,
|
||||
targetSize: CGSize
|
||||
) -> CALayer {
|
||||
return _makeSVGLayer(
|
||||
from: svg,
|
||||
in: container,
|
||||
targetSize: targetSize
|
||||
)
|
||||
}
|
||||
|
||||
@objc public static func _makeSVGLayer(
|
||||
from svg: SVGKImage,
|
||||
in container: CALayer,
|
||||
at normalizedPosition: CGPoint = .zero,
|
||||
targetSize: CGSize,
|
||||
getWrapperLayer: Bool = true
|
||||
) -> CALayer {
|
||||
|
||||
let svgLayer = svg.caLayerTree!
|
||||
|
||||
svgLayer.transform = CATransform3DIdentity
|
||||
|
||||
var unionRect = CGRect.null
|
||||
func accumulateBounds(_ layer: CALayer) {
|
||||
unionRect = unionRect.union(layer.frame)
|
||||
layer.sublayers?.forEach { accumulateBounds($0) }
|
||||
}
|
||||
accumulateBounds(svgLayer)
|
||||
|
||||
svgLayer.setAffineTransform(
|
||||
CGAffineTransform(
|
||||
translationX: -unionRect.origin.x,
|
||||
y: -unionRect.origin.y
|
||||
)
|
||||
)
|
||||
|
||||
let realPosition = (normalizedPosition == .zero
|
||||
? CGPoint(x: container.bounds.midX,y: container.bounds.midY)
|
||||
: CGPoint(x: container.bounds.width*normalizedPosition.x,y: container.bounds.height*normalizedPosition.y))
|
||||
|
||||
let wrapper = CALayer()
|
||||
wrapper.bounds = CGRect(origin: .zero, size: unionRect.size)
|
||||
wrapper.anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
wrapper.position = realPosition
|
||||
|
||||
let scale = min(
|
||||
targetSize.width / unionRect.width,
|
||||
targetSize.height / unionRect.height
|
||||
)
|
||||
|
||||
wrapper.setAffineTransform(
|
||||
CGAffineTransform(scaleX: scale, y: scale)
|
||||
)
|
||||
|
||||
wrapper.addSublayer(svgLayer)
|
||||
|
||||
if !getWrapperLayer {
|
||||
return wrapper
|
||||
}
|
||||
|
||||
let wrappedLayer = CALayer()
|
||||
wrappedLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
wrappedLayer.bounds = CGRect(x: 0, y: 0, width: container.bounds.size.width, height: container.bounds.size.height)
|
||||
wrappedLayer.position = CGPoint(x: container.bounds.midX, y: container.bounds.midY)
|
||||
wrappedLayer.insertSublayer(wrapper, at: 0)
|
||||
|
||||
return wrappedLayer
|
||||
}
|
||||
|
||||
@objc public static func changeColor(layer: CALayer, color: UIColor) {
|
||||
if let shape = layer as? CAShapeLayer {
|
||||
shape.fillColor = color.cgColor
|
||||
shape.strokeColor = color.cgColor
|
||||
}
|
||||
layer.sublayers?.forEach { changeColor(layer: $0, color: color) }
|
||||
}
|
||||
|
||||
@objc public static func makeTouchTrackpoint(in view:UIView) -> CAShapeLayer {
|
||||
var trackPoint = CAShapeLayer()
|
||||
let path = UIBezierPath(
|
||||
arcCenter: CGPoint(x: 0, y: 0),
|
||||
radius: GenericUtils.isIPhone() ? 12 : 15,
|
||||
startAngle: 0,
|
||||
endAngle: CGFloat.pi * 2,
|
||||
clockwise: true
|
||||
)
|
||||
|
||||
trackPoint = CAShapeLayer()
|
||||
trackPoint.path = path.cgPath
|
||||
|
||||
trackPoint.fillColor = UIColor.white.withAlphaComponent(0.3).cgColor
|
||||
trackPoint.strokeColor = UIColor.clear.cgColor
|
||||
trackPoint.lineWidth = 0
|
||||
|
||||
view.layer.addSublayer(trackPoint)
|
||||
trackPoint.position = view.center
|
||||
trackPoint.isHidden = true
|
||||
|
||||
return trackPoint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
//
|
||||
// IAPManager.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2026/1/9.
|
||||
// Copyright © 2026 True砖家@Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import StoreKit
|
||||
|
||||
@objc public enum PurchaseStatus: Int {
|
||||
case notPurchased
|
||||
case purchased
|
||||
case revoked
|
||||
}
|
||||
|
||||
@objc public enum PurchaseInterruption: Int {
|
||||
case unlockNow
|
||||
case restore
|
||||
case learnMore
|
||||
case lowOSVersion
|
||||
}
|
||||
|
||||
@objc public class PurchaseInfo: NSObject {
|
||||
@objc public let valid: Bool
|
||||
@objc public let status: PurchaseStatus
|
||||
@objc public let expirationDate: NSDate?
|
||||
|
||||
@objc public init(status: PurchaseStatus, expirationDate: NSDate?) {
|
||||
self.status = status
|
||||
self.expirationDate = expirationDate
|
||||
self.valid = (status == .purchased)
|
||||
&& (expirationDate?.timeIntervalSinceNow ?? .infinity > 0)
|
||||
}
|
||||
}
|
||||
|
||||
@objc public enum AddOnProduct: UInt8, CaseIterable {
|
||||
case PencilProPack
|
||||
|
||||
func productId() -> String {
|
||||
let bundleId = Bundle.main.bundleIdentifier ?? ""
|
||||
switch (self, bundleId) {
|
||||
case (.PencilProPack, "com.voidlink.iOS"):
|
||||
return "com.pencilpro.voidlink.iOS"
|
||||
case (.PencilProPack, "com.voidlinkextreme.iOS"):
|
||||
return "com.pencilpro.voidlinkextreme.iOS"
|
||||
case (.PencilProPack, "com.voidlink.tf.debug10.iOS"):
|
||||
return "com.pencilpro.voidlink.debug.iOS"
|
||||
case (.PencilProPack, "com.voidlink.tf.iOS"):
|
||||
return "com.pencilpro.voidlink.tf.iOS"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func productName() -> String {
|
||||
switch self {
|
||||
case .PencilProPack:
|
||||
return SwiftLocalizationHelper.localizedString(forKey: "Drawing Toolkit")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func productURL() -> String {
|
||||
switch self {
|
||||
case .PencilProPack:
|
||||
return SwiftLocalizationHelper.localizedString(forKey: "PencilProPackURL")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func productDescription() -> String {
|
||||
switch self {
|
||||
case .PencilProPack:
|
||||
return SwiftLocalizationHelper.localizedString(forKey: "PencilProPackDescription")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func purchaseAbortedNotification() -> Notification.Name {
|
||||
switch self {
|
||||
case .PencilProPack:
|
||||
return Notification.Name("PencilProPurchaseAbortedNotification")
|
||||
default:
|
||||
return Notification.Name("")
|
||||
}
|
||||
}
|
||||
|
||||
func purchaseSucceededNotification() -> Notification.Name {
|
||||
switch self {
|
||||
case .PencilProPack:
|
||||
return Notification.Name("PencilProPurchaseSucceededNotification")
|
||||
default:
|
||||
return Notification.Name("")
|
||||
}
|
||||
}
|
||||
|
||||
static func from(productId: String) -> AddOnProduct? {
|
||||
switch productId {
|
||||
case "com.pencilpro.voidlink.iOS",
|
||||
"com.pencilpro.voidlinkextreme.iOS",
|
||||
"com.pencilpro.voidlink.debug.iOS",
|
||||
"com.pencilpro.voidlink.tf.iOS":
|
||||
return .PencilProPack
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public protocol IAPManagerDelegate: NSObjectProtocol {
|
||||
@objc func iapManagerDidFetchProducts(_ products: [String])
|
||||
|
||||
@objc func iapManagerDidFailWithError(_ error: Error)
|
||||
|
||||
@objc func iapManagerDidPurchase(_ product: AddOnProduct)
|
||||
|
||||
@objc func iapManagerDidRestore(_ product: AddOnProduct)
|
||||
}
|
||||
|
||||
@objc public class IAPManager: NSObject {
|
||||
|
||||
@objc public static let shared = IAPManager()
|
||||
@objc public weak var delegate: IAPManagerDelegate?
|
||||
@objc public private(set) var fetchedProductIds: [String] = []
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
if #available(iOS 15.0, *, *) {
|
||||
Task {
|
||||
await listenForTransactions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fetch Products (StoreKit 2)
|
||||
|
||||
@objc public func fetchProducts() {
|
||||
if #available(iOS 15.0, *, *) {
|
||||
Task {
|
||||
await fetchProductsStoreKit2()
|
||||
}
|
||||
} else {
|
||||
fetchProductsLegacy()
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *, *)
|
||||
private func fetchProductsStoreKit2() async {
|
||||
let ids = Set(AddOnProduct.allCases.map { $0.productId() })
|
||||
do {
|
||||
let sk2Products = try await Product.products(for: ids)
|
||||
let idsFetched = sk2Products.map { $0.id }
|
||||
await MainActor.run {
|
||||
self.delegate?.iapManagerDidFetchProducts(idsFetched)
|
||||
self.fetchedProductIds = idsFetched
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.delegate?.iapManagerDidFailWithError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public var skProducts: [SKProduct] = []
|
||||
private var productsRequest: SKProductsRequest?
|
||||
private func fetchProductsLegacy() {
|
||||
let productIds = Set(AddOnProduct.allCases.map { $0.productId() })
|
||||
|
||||
|
||||
guard productsRequest == nil else { return }
|
||||
|
||||
guard productIds.count>0 else { return }
|
||||
|
||||
fetchedProductIds = Array(productIds)
|
||||
productsRequest = SKProductsRequest(
|
||||
productIdentifiers: productIds
|
||||
)
|
||||
productsRequest?.delegate = self
|
||||
productsRequest?.start()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Purchase (StoreKit 2)
|
||||
|
||||
@objc public func purchase(_ product: AddOnProduct) {
|
||||
if #available(iOS 15.0, *, *) {
|
||||
Task {
|
||||
await purchaseStoreKit2(product)
|
||||
}
|
||||
} else {
|
||||
purchaseLegacy(product)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *, *)
|
||||
private func purchaseStoreKit2(_ product: AddOnProduct) async {
|
||||
let pid = product.productId()
|
||||
do {
|
||||
let products = try await Product.products(for: [pid])
|
||||
guard let sk2p = products.first else {
|
||||
throw NSError(
|
||||
domain: "IAPManager",
|
||||
code: -2,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Product not found"]
|
||||
)
|
||||
}
|
||||
|
||||
let result = try await sk2p.purchase()
|
||||
|
||||
switch result {
|
||||
case .success(let verification):
|
||||
switch verification {
|
||||
case .verified(let transaction):
|
||||
await MainActor.run {
|
||||
self.delegate?.iapManagerDidPurchase(product)
|
||||
}
|
||||
await transaction.finish()
|
||||
IAPManager.handlePurchaseSuccess(product)
|
||||
NotificationCenter.default.post(name: product.purchaseSucceededNotification(), object: nil)
|
||||
case .unverified(_, let err):
|
||||
NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.unlockNow, userInfo:["interruption": PurchaseInterruption.unlockNow.rawValue])
|
||||
await MainActor.run {
|
||||
self.delegate?.iapManagerDidFailWithError(err)
|
||||
}
|
||||
}
|
||||
case .userCancelled:
|
||||
NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.unlockNow, userInfo:["interruption": PurchaseInterruption.unlockNow.rawValue])
|
||||
break
|
||||
default:
|
||||
NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.unlockNow, userInfo:["interruption": PurchaseInterruption.unlockNow.rawValue])
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.unlockNow, userInfo:["interruption": PurchaseInterruption.unlockNow.rawValue])
|
||||
self.delegate?.iapManagerDidFailWithError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc static func handlePurchaseSuccess(_ product: AddOnProduct) {
|
||||
switch product {
|
||||
case .PencilProPack:
|
||||
let dataMan = DataManager()
|
||||
let settings = dataMan.retrieveSettings()
|
||||
settings?.onscreenControls = 1
|
||||
dataMan.saveData()
|
||||
let profileMan = OSCProfilesManager.sharedManager(.zero)
|
||||
profileMan.updateDefaultTemplates()
|
||||
let profiles = profileMan.getAllProfiles()
|
||||
guard profiles.count > 1 else { break }
|
||||
guard let targetProfile = profiles[1] as? OSCProfile else { break }
|
||||
if targetProfile.name == "Pencil Pro" {
|
||||
profileMan.setProfileToSelected(1)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func purchaseLegacy(_ product: AddOnProduct) {
|
||||
let productId = product.productId()
|
||||
print("products \(skProducts.count)")
|
||||
guard let skProduct = skProducts.first(
|
||||
where: {
|
||||
print("$0.productIdentifier \($0.productIdentifier), productId \(productId)")
|
||||
return $0.productIdentifier == productId
|
||||
}
|
||||
) else { return }
|
||||
let payment = SKPayment(product: skProduct)
|
||||
SKPaymentQueue.default().add(payment)
|
||||
}
|
||||
|
||||
// MARK: - Restore (StoreKit 2)
|
||||
|
||||
@objc public func restorePurchasesStoreKit2() {
|
||||
if #available(iOS 15.0, *, *) {
|
||||
Task {
|
||||
await restoreStoreKit2()
|
||||
}
|
||||
}
|
||||
else{
|
||||
restorePurchasesLegacy()
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *, *)
|
||||
private func restoreStoreKit2() async {
|
||||
for await result in Transaction.currentEntitlements {
|
||||
if case .verified(let transaction) = result,
|
||||
let adp = AddOnProduct.from(productId: transaction.productID) {
|
||||
await MainActor.run {
|
||||
self.delegate?.iapManagerDidRestore(adp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func restorePurchasesLegacy() {
|
||||
SKPaymentQueue.default().restoreCompletedTransactions()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Listen update
|
||||
|
||||
@available(iOS 15.0, *, *)
|
||||
private func listenForTransactions() async {
|
||||
for await verificationResult in Transaction.updates {
|
||||
if case .verified(let transaction) = verificationResult,
|
||||
let adp = AddOnProduct.from(productId: transaction.productID) {
|
||||
await MainActor.run {
|
||||
// 这里把支付成功回调出来
|
||||
self.delegate?.iapManagerDidPurchase(adp)
|
||||
}
|
||||
await transaction.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Utils
|
||||
@objc static public func checkPurchaseInfo(
|
||||
_ product: AddOnProduct,
|
||||
completion: @escaping (PurchaseInfo) -> Void
|
||||
) {
|
||||
let productID = product.productId()
|
||||
if #available(iOS 15.0, *) {
|
||||
Task {
|
||||
var status: PurchaseStatus = .notPurchased
|
||||
var expiration: Date? = nil
|
||||
for await verificationResult in Transaction.currentEntitlements {
|
||||
if case .verified(let transaction) = verificationResult,
|
||||
transaction.productID == productID {
|
||||
|
||||
// 有退款 / 撤销
|
||||
if transaction.revocationDate != nil {
|
||||
status = .revoked
|
||||
} else {
|
||||
status = .purchased
|
||||
expiration = transaction.expirationDate
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
let info = PurchaseInfo(status: status, expirationDate: expiration as NSDate?)
|
||||
|
||||
await MainActor.run {
|
||||
completion(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.lowOSVersion, userInfo:["interruption": PurchaseInterruption.lowOSVersion.rawValue])
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
static public func checkPurchaseInfo(
|
||||
_ product: AddOnProduct
|
||||
) async -> PurchaseInfo {
|
||||
await withCheckedContinuation { continuation in
|
||||
checkPurchaseInfo(product) { info in
|
||||
continuation.resume(returning: info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc static public func inAppPurchaseAction(viewController: UIViewController, product: AddOnProduct){
|
||||
|
||||
let alert = UIAlertController(title: product.productName(),
|
||||
message: SwiftLocalizationHelper.localizedString(forKey: "No purchase found", product.productName()),
|
||||
preferredStyle: .alert)
|
||||
|
||||
let unlockAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Purchase Now"), style: .default) { _ in
|
||||
IAPManager.shared.purchase(product)
|
||||
}
|
||||
|
||||
let restoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Restore Purchase"), style: .default) { _ in
|
||||
if #available(iOS 13.0, *) {
|
||||
Task{
|
||||
do {
|
||||
if #available(iOS 15.0, *) {
|
||||
try await AppStore.sync()
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
} catch {
|
||||
NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.restore, userInfo:["interruption": PurchaseInterruption.restore.rawValue])
|
||||
print("Restore failed: \(error)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
}
|
||||
|
||||
let learnMoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
|
||||
NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.learnMore, userInfo:["interruption": PurchaseInterruption.learnMore.rawValue])
|
||||
if let url = URL(string: product.productURL()) {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
alert.addAction(unlockAction)
|
||||
alert.addAction(restoreAction)
|
||||
alert.addAction(learnMoreAction)
|
||||
|
||||
viewController.present(alert, animated: true, completion: {
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@objc static public func inAppPurchaseURLMessage(viewController: UIViewController, product: AddOnProduct){
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
extension IAPManager: SKProductsRequestDelegate {
|
||||
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
|
||||
skProducts = response.products
|
||||
let productIds = Array(skProducts.map { $0.productIdentifier })
|
||||
delegate?.iapManagerDidFetchProducts(productIds)
|
||||
}
|
||||
|
||||
public func request(_ request: SKRequest, didFailWithError error: Error) {
|
||||
delegate?.iapManagerDidFailWithError(error)
|
||||
}
|
||||
}
|
||||
|
||||
extension IAPManager: SKPaymentTransactionObserver {
|
||||
|
||||
public func paymentQueue(_ queue: SKPaymentQueue,
|
||||
updatedTransactions transactions: [SKPaymentTransaction]) {
|
||||
|
||||
print("paymentQueue \(CACurrentMediaTime())")
|
||||
|
||||
for transaction in transactions {
|
||||
let productId = transaction.payment.productIdentifier
|
||||
guard let product = AddOnProduct.from(productId: productId) else {
|
||||
SKPaymentQueue.default().finishTransaction(transaction)
|
||||
continue
|
||||
}
|
||||
|
||||
print("transaction.transactionState \(transaction.transactionState)")
|
||||
|
||||
switch transaction.transactionState {
|
||||
|
||||
case .purchased:
|
||||
delegate?.iapManagerDidPurchase(product)
|
||||
SKPaymentQueue.default().finishTransaction(transaction)
|
||||
|
||||
case .restored:
|
||||
delegate?.iapManagerDidRestore(product)
|
||||
SKPaymentQueue.default().finishTransaction(transaction)
|
||||
|
||||
case .failed:
|
||||
if let error = transaction.error {
|
||||
delegate?.iapManagerDidFailWithError(error)
|
||||
}
|
||||
SKPaymentQueue.default().finishTransaction(transaction)
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||