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;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 60;
|
objectVersion = 70;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1829CE3E2E2154DF0074A99A /* SceneDelegate.m */; };
|
||||||
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 18341A032DEB662C0094E5F2 /* FixedTintImageView.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 */; };
|
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */; };
|
||||||
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1912E24C33100108730 /* AboutViewController.swift */; };
|
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1912E24C33100108730 /* AboutViewController.swift */; };
|
||||||
1888C1932E24C41000108730 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1902E24C2C800108730 /* AboutView.swift */; };
|
1888C1932E24C41000108730 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1902E24C2C800108730 /* AboutView.swift */; };
|
||||||
188973D32E386A670025E48C /* widgetTemplatesIPhone.bin in Resources */ = {isa = PBXBuildFile; fileRef = 188973D22E386A670025E48C /* widgetTemplatesIPhone.bin */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */; };
|
||||||
84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B072C29ACCA00A77989 /* LayoutOnScreenControls.m */; };
|
84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B072C29ACCA00A77989 /* LayoutOnScreenControls.m */; };
|
||||||
84351B0F2C29ACCB00A77989 /* OnScreenButtonState.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B092C29ACCA00A77989 /* OnScreenButtonState.m */; };
|
84351B0F2C29ACCB00A77989 /* OnScreenButtonState.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B092C29ACCA00A77989 /* OnScreenButtonState.m */; };
|
||||||
@@ -151,23 +172,38 @@
|
|||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
FBDE86E519F82297001C18A8 /* UIAppView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIAppView.m; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
18A0EE282EFBCBFB00BA853A /* SVGs */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SVGs; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
FB290CEB19B2C406004C83CF /* Frameworks */ = {
|
FB290CEB19B2C406004C83CF /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
18A0EE272EFB9B5A00BA853A /* SVGKitSwift in Frameworks */,
|
||||||
|
18A0EE252EFB9B5A00BA853A /* SVGKit in Frameworks */,
|
||||||
9890CF6B203B7EE1006C4B06 /* libxml2.tbd in Frameworks */,
|
9890CF6B203B7EE1006C4B06 /* libxml2.tbd in Frameworks */,
|
||||||
98CFB82F1CAD481B0048EF74 /* libmoonlight-common.a in Frameworks */,
|
98CFB82F1CAD481B0048EF74 /* libmoonlight-common.a in Frameworks */,
|
||||||
FB8946ED19F6AFE800339C8A /* libopus.a in Frameworks */,
|
FB8946ED19F6AFE800339C8A /* libopus.a in Frameworks */,
|
||||||
98882A082AF60F7400C5A11C /* libavutil.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 */,
|
FB290CF419B2C406004C83CF /* CoreGraphics.framework in Frameworks */,
|
||||||
FB290CF819B2C406004C83CF /* CoreData.framework in Frameworks */,
|
FB290CF819B2C406004C83CF /* CoreData.framework in Frameworks */,
|
||||||
98181BED2791281100E43572 /* CoreMotion.framework in Frameworks */,
|
98181BED2791281100E43572 /* CoreMotion.framework in Frameworks */,
|
||||||
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */,
|
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */,
|
||||||
98882A072AF60F7200C5A11C /* libavformat.a in Frameworks */,
|
98882A072AF60F7200C5A11C /* libavformat.a in Frameworks */,
|
||||||
FB290CF619B2C406004C83CF /* UIKit.framework in Frameworks */,
|
FB290CF619B2C406004C83CF /* UIKit.framework in Frameworks */,
|
||||||
|
18780CC72F10D66000B90483 /* StoreKit.framework in Frameworks */,
|
||||||
FB290CF219B2C406004C83CF /* Foundation.framework in Frameworks */,
|
FB290CF219B2C406004C83CF /* Foundation.framework in Frameworks */,
|
||||||
FB4679011A57048000377732 /* CoreFoundation.framework in Frameworks */,
|
FB4679011A57048000377732 /* CoreFoundation.framework in Frameworks */,
|
||||||
|
182760FA2E83E57D00EC1C5C /* DequeModule in Frameworks */,
|
||||||
98181BE92791277400E43572 /* libSDL2.a in Frameworks */,
|
98181BE92791277400E43572 /* libSDL2.a in Frameworks */,
|
||||||
98882A062AF60F7000C5A11C /* libavcodec.a in Frameworks */,
|
98882A062AF60F7000C5A11C /* libavcodec.a in Frameworks */,
|
||||||
);
|
);
|
||||||
@@ -559,6 +606,7 @@
|
|||||||
FB290CF019B2C406004C83CF /* Frameworks */ = {
|
FB290CF019B2C406004C83CF /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
18780CC62F10D66000B90483 /* StoreKit.framework */,
|
||||||
98882A032AF60F5300C5A11C /* libavcodec.a */,
|
98882A032AF60F5300C5A11C /* libavcodec.a */,
|
||||||
98882A052AF60F5300C5A11C /* libavformat.a */,
|
98882A052AF60F5300C5A11C /* libavformat.a */,
|
||||||
98882A0A2AF60F9000C5A11C /* libavcodec.a */,
|
98882A0A2AF60F9000C5A11C /* libavcodec.a */,
|
||||||
@@ -612,16 +660,17 @@
|
|||||||
FB290D0319B2C406004C83CF /* AppDelegate.m */,
|
FB290D0319B2C406004C83CF /* AppDelegate.m */,
|
||||||
848F45682DDCD0D600581E36 /* HostCardView.h */,
|
848F45682DDCD0D600581E36 /* HostCardView.h */,
|
||||||
848F45692DDCD12500581E36 /* HostCardView.m */,
|
848F45692DDCD12500581E36 /* HostCardView.m */,
|
||||||
841BAC8F2DE35BEC00DA639E /* ThemeManager.h */,
|
2A7113FC2F5A6C65001D852D /* ThemeManager.swift */,
|
||||||
841BAC912DE35D6B00DA639E /* ThemeManager.m */,
|
|
||||||
847D2FD82DDA00C800937A62 /* MenuSectionView.h */,
|
847D2FD82DDA00C800937A62 /* MenuSectionView.h */,
|
||||||
847D2FDA2DDA015F00937A62 /* MenuSectionView.m */,
|
847D2FDA2DDA015F00937A62 /* MenuSectionView.m */,
|
||||||
FBDE86E419F82297001C18A8 /* UIAppView.h */,
|
FBDE86E419F82297001C18A8 /* UIAppView.h */,
|
||||||
FBDE86E519F82297001C18A8 /* UIAppView.m */,
|
FBDE86E519F82297001C18A8 /* UIAppView.m */,
|
||||||
|
180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */,
|
||||||
1888C1902E24C2C800108730 /* AboutView.swift */,
|
1888C1902E24C2C800108730 /* AboutView.swift */,
|
||||||
1888C1912E24C33100108730 /* AboutViewController.swift */,
|
1888C1912E24C33100108730 /* AboutViewController.swift */,
|
||||||
FB89463719F6473800339C8A /* Launch Screen.xib */,
|
FB89463719F6473800339C8A /* Launch Screen.xib */,
|
||||||
FB290D0819B2C406004C83CF /* Images.xcassets */,
|
FB290D0819B2C406004C83CF /* Images.xcassets */,
|
||||||
|
18A0EE282EFBCBFB00BA853A /* SVGs */,
|
||||||
FB290D0519B2C406004C83CF /* Limelight.xcdatamodeld */,
|
FB290D0519B2C406004C83CF /* Limelight.xcdatamodeld */,
|
||||||
FB1D59981BBCCD7E00F482CA /* AppCollectionView.h */,
|
FB1D59981BBCCD7E00F482CA /* AppCollectionView.h */,
|
||||||
FB1D59991BBCCD7E00F482CA /* AppCollectionView.m */,
|
FB1D59991BBCCD7E00F482CA /* AppCollectionView.m */,
|
||||||
@@ -677,18 +726,27 @@
|
|||||||
9827E7A7251436EA00F25707 /* HapticContext.h */,
|
9827E7A7251436EA00F25707 /* HapticContext.h */,
|
||||||
840751B12BF49B5A0096B657 /* CustomTapGestureRecognizer.h */,
|
840751B12BF49B5A0096B657 /* CustomTapGestureRecognizer.h */,
|
||||||
840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */,
|
840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */,
|
||||||
9819CC12254F107A008A7C8E /* RelativeTouchHandler.h */,
|
|
||||||
843CC7F42C1E778000BBC475 /* NativeTouchHandler.h */,
|
|
||||||
844CD0E82C7F847E004861A7 /* PureNativeTouchHandler.h */,
|
844CD0E82C7F847E004861A7 /* PureNativeTouchHandler.h */,
|
||||||
844CD0EA2C7F8500004861A7 /* PureNativeTouchHandler.m */,
|
844CD0EA2C7F8500004861A7 /* PureNativeTouchHandler.m */,
|
||||||
|
843CC7F42C1E778000BBC475 /* NativeTouchHandler.h */,
|
||||||
843CC7F52C1E7A1700BBC475 /* NativeTouchHandler.m */,
|
843CC7F52C1E7A1700BBC475 /* NativeTouchHandler.m */,
|
||||||
|
9819CC12254F107A008A7C8E /* RelativeTouchHandler.h */,
|
||||||
9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */,
|
9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */,
|
||||||
9819CC20254F180F008A7C8E /* AbsoluteTouchHandler.h */,
|
9819CC20254F180F008A7C8E /* AbsoluteTouchHandler.h */,
|
||||||
9819CC21254F180F008A7C8E /* AbsoluteTouchHandler.m */,
|
9819CC21254F180F008A7C8E /* AbsoluteTouchHandler.m */,
|
||||||
988FCD3F293B091B003050E2 /* KeyboardInputField.h */,
|
988FCD3F293B091B003050E2 /* KeyboardInputField.h */,
|
||||||
988FCD40293B091B003050E2 /* KeyboardInputField.m */,
|
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 */,
|
84E1CDEB2C4F67D300547904 /* CommandManager.swift */,
|
||||||
84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */,
|
84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */,
|
||||||
|
1817B61D2E6FD04D000D938D /* SafeTimer.swift */,
|
||||||
);
|
);
|
||||||
path = Input;
|
path = Input;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -728,6 +786,10 @@
|
|||||||
FB89461E19F646E200339C8A /* Utility */ = {
|
FB89461E19F646E200339C8A /* Utility */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
18729EDD2E8BD68800101194 /* UUIDHelper.swift */,
|
||||||
|
2AC1E3822F507D80004F76C0 /* GenericUtils.swift */,
|
||||||
|
18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */,
|
||||||
|
18780CC92F10D6CC00B90483 /* IAPManager.swift */,
|
||||||
97E20DD42E2164A9000D5DCA /* BandwidthTracker.h */,
|
97E20DD42E2164A9000D5DCA /* BandwidthTracker.h */,
|
||||||
97E20DD52E2164A9000D5DCA /* BandwidthTracker.m */,
|
97E20DD52E2164A9000D5DCA /* BandwidthTracker.m */,
|
||||||
97E20DD62E2164A9000D5DCA /* FloatBuffer.h */,
|
97E20DD62E2164A9000D5DCA /* FloatBuffer.h */,
|
||||||
@@ -809,6 +871,7 @@
|
|||||||
FB8946E919F6AFB800339C8A /* lib */ = {
|
FB8946E919F6AFB800339C8A /* lib */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
180345E02E819877004AF36F /* libopus-sim.a */,
|
||||||
FB8946EA19F6AFB800339C8A /* libopus.a */,
|
FB8946EA19F6AFB800339C8A /* libopus.a */,
|
||||||
);
|
);
|
||||||
path = lib;
|
path = lib;
|
||||||
@@ -888,6 +951,9 @@
|
|||||||
dependencies = (
|
dependencies = (
|
||||||
98AB2E861CAD468B0089BB98 /* PBXTargetDependency */,
|
98AB2E861CAD468B0089BB98 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
18A0EE282EFBCBFB00BA853A /* SVGs */,
|
||||||
|
);
|
||||||
name = VoidLink;
|
name = VoidLink;
|
||||||
productName = Limelight;
|
productName = Limelight;
|
||||||
productReference = FB290CEE19B2C406004C83CF /* VoidLink.app */;
|
productReference = FB290CEE19B2C406004C83CF /* VoidLink.app */;
|
||||||
@@ -922,6 +988,8 @@
|
|||||||
mainGroup = FB290CE519B2C406004C83CF;
|
mainGroup = FB290CE519B2C406004C83CF;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */,
|
F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */,
|
||||||
|
182760F52E83E2EA00EC1C5C /* XCRemoteSwiftPackageReference "swift-collections" */,
|
||||||
|
18A0EE232EFB9B5A00BA853A /* XCRemoteSwiftPackageReference "SVGKit" */,
|
||||||
);
|
);
|
||||||
productRefGroup = FB290CEF19B2C406004C83CF /* Products */;
|
productRefGroup = FB290CEF19B2C406004C83CF /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -985,7 +1053,9 @@
|
|||||||
FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */,
|
FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */,
|
||||||
97E20DDF2E2164A9000D5DCA /* Frame.m in Sources */,
|
97E20DDF2E2164A9000D5DCA /* Frame.m in Sources */,
|
||||||
97E20DE02E2164A9000D5DCA /* FrameQueue.m in Sources */,
|
97E20DE02E2164A9000D5DCA /* FrameQueue.m in Sources */,
|
||||||
|
18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */,
|
||||||
97E20DE12E2164A9000D5DCA /* FloatBuffer.m in Sources */,
|
97E20DE12E2164A9000D5DCA /* FloatBuffer.m in Sources */,
|
||||||
|
18FCADAF2ECAC23200787A96 /* PencilHandler.swift in Sources */,
|
||||||
97E20DE22E2164A9000D5DCA /* PaddedLabel.m in Sources */,
|
97E20DE22E2164A9000D5DCA /* PaddedLabel.m in Sources */,
|
||||||
97E20DE32E2164A9000D5DCA /* BandwidthTracker.m in Sources */,
|
97E20DE32E2164A9000D5DCA /* BandwidthTracker.m in Sources */,
|
||||||
84591A512BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m in Sources */,
|
84591A512BE1625E006AAB10 /* CustomEdgeSlideGestureRecognizer.m in Sources */,
|
||||||
@@ -1005,8 +1075,10 @@
|
|||||||
97E21BC12E21882D000D5DCA /* imgui_widgets.cpp in Sources */,
|
97E21BC12E21882D000D5DCA /* imgui_widgets.cpp in Sources */,
|
||||||
97E21BC22E21882D000D5DCA /* imgui.cpp in Sources */,
|
97E21BC22E21882D000D5DCA /* imgui.cpp in Sources */,
|
||||||
97E21BC32E21882D000D5DCA /* imgui_draw.cpp in Sources */,
|
97E21BC32E21882D000D5DCA /* imgui_draw.cpp in Sources */,
|
||||||
|
18A765BD2ECD51FB004784AA /* ControllerUtil.swift in Sources */,
|
||||||
84E1CDEC2C4F67D300547904 /* CommandManager.swift in Sources */,
|
84E1CDEC2C4F67D300547904 /* CommandManager.swift in Sources */,
|
||||||
847D2FDB2DDA015F00937A62 /* MenuSectionView.m in Sources */,
|
847D2FDB2DDA015F00937A62 /* MenuSectionView.m in Sources */,
|
||||||
|
18A0EE2C2EFBCCFD00BA853A /* GraphicUtils.swift in Sources */,
|
||||||
FB53E1431BE5DC4400CD6ECE /* IdManager.m in Sources */,
|
FB53E1431BE5DC4400CD6ECE /* IdManager.m in Sources */,
|
||||||
189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */,
|
189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */,
|
||||||
9897B6A1221260EF00966419 /* VoidController.m in Sources */,
|
9897B6A1221260EF00966419 /* VoidController.m in Sources */,
|
||||||
@@ -1014,14 +1086,18 @@
|
|||||||
FB89462919F646E200339C8A /* mkcert.c in Sources */,
|
FB89462919F646E200339C8A /* mkcert.c in Sources */,
|
||||||
FB9AFD281A7C84ED00872C98 /* HttpResponse.m in Sources */,
|
FB9AFD281A7C84ED00872C98 /* HttpResponse.m in Sources */,
|
||||||
FB89463019F646E200339C8A /* StreamConfiguration.m in Sources */,
|
FB89463019F646E200339C8A /* StreamConfiguration.m in Sources */,
|
||||||
|
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */,
|
||||||
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */,
|
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */,
|
||||||
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */,
|
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */,
|
||||||
|
2AC1E3832F507D80004F76C0 /* GenericUtils.swift in Sources */,
|
||||||
97E21BB52E2186A3000D5DCA /* imgui_impl_metal.mm in Sources */,
|
97E21BB52E2186A3000D5DCA /* imgui_impl_metal.mm in Sources */,
|
||||||
9777AAF32E2CF7A400D7FBAF /* Shaders-Linear.metal in Sources */,
|
9777AAF32E2CF7A400D7FBAF /* Shaders-Linear.metal in Sources */,
|
||||||
84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */,
|
84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */,
|
||||||
97E20DEA2E216532000D5DCA /* SceneDelegate.m in Sources */,
|
97E20DEA2E216532000D5DCA /* SceneDelegate.m in Sources */,
|
||||||
|
1817B61E2E6FD04D000D938D /* SafeTimer.swift in Sources */,
|
||||||
FBD3495019FF2174002D2A60 /* SettingsViewController.m in Sources */,
|
FBD3495019FF2174002D2A60 /* SettingsViewController.m in Sources */,
|
||||||
843CC7F02C1E757D00BBC475 /* NativeTouchPointer.m in Sources */,
|
843CC7F02C1E757D00BBC475 /* NativeTouchPointer.m in Sources */,
|
||||||
|
2A7113FD2F5A6C65001D852D /* ThemeManager.swift in Sources */,
|
||||||
FB89462C19F646E200339C8A /* HttpManager.m in Sources */,
|
FB89462C19F646E200339C8A /* HttpManager.m in Sources */,
|
||||||
FB89462D19F646E200339C8A /* MDNSManager.m in Sources */,
|
FB89462D19F646E200339C8A /* MDNSManager.m in Sources */,
|
||||||
84351B0F2C29ACCB00A77989 /* OnScreenButtonState.m in Sources */,
|
84351B0F2C29ACCB00A77989 /* OnScreenButtonState.m in Sources */,
|
||||||
@@ -1029,7 +1105,8 @@
|
|||||||
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */,
|
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */,
|
||||||
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */,
|
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */,
|
||||||
FB4678FA1A55FFAD00377732 /* DiscoveryManager.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 */,
|
98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */,
|
||||||
FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */,
|
FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */,
|
||||||
FBD1C8E21A8AD71400C6703C /* Logger.m in Sources */,
|
FBD1C8E21A8AD71400C6703C /* Logger.m in Sources */,
|
||||||
@@ -1048,6 +1125,7 @@
|
|||||||
84351B2C2C29AD6100A77989 /* OSCProfilesTableViewController.m in Sources */,
|
84351B2C2C29AD6100A77989 /* OSCProfilesTableViewController.m in Sources */,
|
||||||
98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */,
|
98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */,
|
||||||
1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */,
|
1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */,
|
||||||
|
18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */,
|
||||||
FB89462819F646E200339C8A /* CryptoManager.m in Sources */,
|
FB89462819F646E200339C8A /* CryptoManager.m in Sources */,
|
||||||
DC1F5A07206436B20037755F /* ConnectionHelper.m in Sources */,
|
DC1F5A07206436B20037755F /* ConnectionHelper.m in Sources */,
|
||||||
FB89462E19F646E200339C8A /* PairManager.m in Sources */,
|
FB89462E19F646E200339C8A /* PairManager.m in Sources */,
|
||||||
@@ -1065,13 +1143,17 @@
|
|||||||
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */,
|
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */,
|
||||||
FBD349621A0089F6002D2A60 /* DataManager.m in Sources */,
|
FBD349621A0089F6002D2A60 /* DataManager.m in Sources */,
|
||||||
97E20DE72E216510000D5DCA /* ImGuiRenderer.mm in Sources */,
|
97E20DE72E216510000D5DCA /* ImGuiRenderer.mm in Sources */,
|
||||||
|
18B317032EE67A3A009B62C2 /* InertialScroller.swift in Sources */,
|
||||||
FB4A23B81A9D3637004D2EF2 /* LoadingFrameViewController.m in Sources */,
|
FB4A23B81A9D3637004D2EF2 /* LoadingFrameViewController.m in Sources */,
|
||||||
FB9AFD3A1A7E05CE00872C98 /* ServerInfoResponse.m in Sources */,
|
FB9AFD3A1A7E05CE00872C98 /* ServerInfoResponse.m in Sources */,
|
||||||
84351B2B2C29AD6100A77989 /* LayoutOnScreenControlsViewController.m in Sources */,
|
84351B2B2C29AD6100A77989 /* LayoutOnScreenControlsViewController.m in Sources */,
|
||||||
84351B5C2C29ADBE00A77989 /* ProfileTableViewCell.m in Sources */,
|
84351B5C2C29ADBE00A77989 /* ProfileTableViewCell.m in Sources */,
|
||||||
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */,
|
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */,
|
||||||
|
18416EA42EBB3DCE00AE0124 /* UITouchUtil.swift in Sources */,
|
||||||
FB89463119F646E200339C8A /* StreamManager.m in Sources */,
|
FB89463119F646E200339C8A /* StreamManager.m in Sources */,
|
||||||
|
18780CCA2F10D6CC00B90483 /* IAPManager.swift in Sources */,
|
||||||
84351B2D2C29AD6100A77989 /* ToolBarContainerView.m in Sources */,
|
84351B2D2C29AD6100A77989 /* ToolBarContainerView.m in Sources */,
|
||||||
|
18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */,
|
||||||
988FCD41293B091B003050E2 /* KeyboardInputField.m in Sources */,
|
988FCD41293B091B003050E2 /* KeyboardInputField.m in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -1260,7 +1342,7 @@
|
|||||||
"$(PROJECT_DIR)/Limelight/Database",
|
"$(PROJECT_DIR)/Limelight/Database",
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = "VoidLink/Limelight-Info.plist";
|
INFOPLIST_FILE = "VoidLink/Limelight-Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "VoidLink - beta";
|
INFOPLIST_KEY_CFBundleDisplayName = VoidLink;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -1271,6 +1353,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-32",
|
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-32",
|
||||||
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-64",
|
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-64",
|
||||||
|
"$(PROJECT_DIR)/libs/opus/lib",
|
||||||
);
|
);
|
||||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -1325,7 +1408,7 @@
|
|||||||
"$(PROJECT_DIR)/Limelight/Database",
|
"$(PROJECT_DIR)/Limelight/Database",
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = "VoidLink/Limelight-Info.plist";
|
INFOPLIST_FILE = "VoidLink/Limelight-Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "VoidLink - beta";
|
INFOPLIST_KEY_CFBundleDisplayName = VoidLink;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -1336,6 +1419,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-32",
|
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-32",
|
||||||
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-64",
|
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-64",
|
||||||
|
"$(PROJECT_DIR)/libs/opus/lib",
|
||||||
);
|
);
|
||||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -1388,6 +1472,22 @@
|
|||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference 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" */ = {
|
F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/krzyzanowskim/OpenSSL-Package.git";
|
repositoryURL = "https://github.com/krzyzanowskim/OpenSSL-Package.git";
|
||||||
@@ -1399,6 +1499,31 @@
|
|||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency 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 */ = {
|
F74BEF9B2C1A705600224667 /* OpenSSL */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */;
|
package = F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */;
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "f17bf6f1131c7255a75ffd6a989b69573cb49c332e612dffb429905d8d7afe72",
|
"originHash" : "d1fc50a28d75017e32553df2b233f8a7e2cf1542da32728ce6722b200f745608",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "cocoalumberjack",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "a9ed4b6f9bdedce7d77046f43adfb8ce1fd54114",
|
||||||
|
"version" : "3.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "openssl-package",
|
"identity" : "openssl-package",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -9,6 +18,33 @@
|
|||||||
"revision" : "b9eb055fdf73e595cb4b9f665d13dc85975bf80a",
|
"revision" : "b9eb055fdf73e595cb4b9f665d13dc85975bf80a",
|
||||||
"version" : "3.3.2000"
|
"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
|
"version" : 3
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ public struct AboutView: View {
|
|||||||
public var aboutVC:UIViewController
|
public var aboutVC:UIViewController
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack(spacing: 10) {
|
VStack(spacing: 7) {
|
||||||
// App 图标
|
// App 图标
|
||||||
Image(uiImage: UIImage(named: "AppIconMedium") ?? UIImage())
|
Image(uiImage: UIImage(named: "AppIconMedium") ?? UIImage())
|
||||||
// .resizable()
|
// .resizable()
|
||||||
.resizable(capInsets:EdgeInsets(top: 3.5, leading: 3.5, bottom: 3.5, trailing: 3.5))
|
.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))
|
.clipShape(RoundedRectangle(cornerRadius: 28))
|
||||||
|
|
||||||
// App 名称
|
// App 名称
|
||||||
@@ -35,31 +35,41 @@ public struct AboutView: View {
|
|||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 说明文字
|
|
||||||
Text(SwiftLocalizationHelper.localizedString(forKey: "From the player community, to the player community."))
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.lineLimit(nil)
|
|
||||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
|
||||||
|
|
||||||
if Locale.preferredLanguages.first!.hasPrefix("zh-Hans") {
|
if Locale.preferredLanguages.first!.hasPrefix("zh-Hans") {
|
||||||
if #available(iOS 14.0, *) {
|
if #available(iOS 14.0, *) {
|
||||||
Text("闽ICP备17012590号-3A\n主办单位:福州创图信息技术有限公司").font(.caption2)
|
Text("闽ICP备17012590号-3A 主办单位:福州创图信息技术有限公司").font(.caption2)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.lineLimit(nil)
|
.lineLimit(nil)
|
||||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||||
.padding()
|
//.padding()
|
||||||
} else {
|
} else {
|
||||||
Text("闽ICP备17012590号-3A\n主办单位:福州创图信息技术有限公司").font(.caption)
|
Text("闽ICP备17012590号-3A 主办单位:福州创图信息技术有限公司").font(.caption)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.lineLimit(nil)
|
.lineLimit(nil)
|
||||||
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
.frame(maxWidth: 570) // ✅ 避免 Text 被拉得太宽无法换行
|
||||||
.padding()
|
//.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 #available(iOS 14.0, *) {
|
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)
|
.padding(.top, 10)
|
||||||
Spacer()
|
Spacer()
|
||||||
// OK 按钮
|
// OK 按钮
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import SwiftUI
|
|||||||
])
|
])
|
||||||
|
|
||||||
// 🎯 设置弹窗大小
|
// 🎯 设置弹窗大小
|
||||||
preferredContentSize = CGSize(width: 530, height: 450)
|
preferredContentSize = CGSize(width: 530, height: 430)
|
||||||
|
|
||||||
// ⚠️ 一定要设置以下样式才会生效 preferredContentSize
|
// ⚠️ 一定要设置以下样式才会生效 preferredContentSize
|
||||||
modalPresentationStyle = .formSheet
|
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
|
@interface DataManager : NSObject
|
||||||
|
|
||||||
typedef NS_ENUM(NSUInteger, UINavigationBarHeight) {
|
typedef NS_ENUM(NSInteger, ControllerGyroSwitchMode) {
|
||||||
UINavigationBarHeightIPad = 50,
|
ControllerGyroSwitchDisabled,
|
||||||
UINavigationBarHeightIPhone = 44
|
ControllerGyroSwitchPressToToggle,
|
||||||
|
ControllerGyroSwitchHoldDown
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, TouchMode) {
|
typedef NS_ENUM(NSInteger, TouchMode) {
|
||||||
RelativeTouch,
|
RelativeTouch,
|
||||||
NativeTouch,
|
NativeTouch,
|
||||||
AbsoluteTouch,
|
AbsoluteTouch,
|
||||||
|
TouchDisabled,
|
||||||
NativeTouchOnly
|
NativeTouchOnly
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,7 +44,20 @@ typedef NS_ENUM(NSInteger, GyroMode) {
|
|||||||
AlwaysController
|
AlwaysController
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, HapticEnginePreference) {
|
||||||
|
HapticEngineAuto,
|
||||||
|
RumbleDevice,
|
||||||
|
LeftRightSwapped,
|
||||||
|
RumbleOff
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, ControllerMouseStick) {
|
||||||
|
LeftStickToMouse,
|
||||||
|
RightStickToMouse,
|
||||||
|
};
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, FramePacingMode) {
|
typedef NS_ENUM(NSInteger, FramePacingMode) {
|
||||||
|
FramePacingModeOff,
|
||||||
FramePacingModeLegacy,
|
FramePacingModeLegacy,
|
||||||
FramePacingModeQueue
|
FramePacingModeQueue
|
||||||
};
|
};
|
||||||
@@ -53,7 +68,18 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
|
|||||||
RemoveSettingItem,
|
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
|
framerate:(NSInteger)framerate
|
||||||
height:(NSInteger)height
|
height:(NSInteger)height
|
||||||
width:(NSInteger)width
|
width:(NSInteger)width
|
||||||
@@ -69,25 +95,32 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
|
|||||||
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
|
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
|
||||||
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
|
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
|
||||||
gyroSensitivity:(CGFloat)gyroSensitivity
|
gyroSensitivity:(CGFloat)gyroSensitivity
|
||||||
|
localVolume:(CGFloat)localVolume
|
||||||
|
micVolume:(CGFloat)micVolume
|
||||||
touchMoveEventInterval:(NSInteger)touchMoveEventInterval
|
touchMoveEventInterval:(NSInteger)touchMoveEventInterval
|
||||||
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
|
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
|
||||||
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
|
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
|
||||||
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
|
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
|
||||||
showKeyboardToolbar:(BOOL)showKeyboardToolbar
|
showKeyboardToolbar:(BOOL)showKeyboardToolbar
|
||||||
optimizeGames:(BOOL)optimizeGames
|
optimizeGames:(BOOL)optimizeGames
|
||||||
multiController:(BOOL)multiController
|
multiController:(BOOL)multiController
|
||||||
|
buttonVisualFeedback:(BOOL)buttonVisualFeedback
|
||||||
|
touchPointTracking:(BOOL)touchPointTracking
|
||||||
swapABXYButtons:(BOOL)swapABXYButtons
|
swapABXYButtons:(BOOL)swapABXYButtons
|
||||||
audioOnPC:(BOOL)audioOnPC
|
audioOnPC:(BOOL)audioOnPC
|
||||||
|
redirectMic:(BOOL)redirectMic
|
||||||
|
useBuiltinMic:(BOOL)useBuiltinMic
|
||||||
preferredCodec:(uint32_t)preferredCodec
|
preferredCodec:(uint32_t)preferredCodec
|
||||||
enableYUV444:(BOOL)enableYUV444
|
enableYUV444:(BOOL)enableYUV444
|
||||||
enablePIP:(BOOL)enablePIP
|
enablePIP:(BOOL)enablePIP
|
||||||
|
fullColorRange:(BOOL)fullColorRange
|
||||||
enableHdr:(BOOL)enableHdr
|
enableHdr:(BOOL)enableHdr
|
||||||
btMouseSupport:(BOOL)btMouseSupport
|
btMouseSupport:(BOOL)btMouseSupport
|
||||||
// absoluteTouchMode:(BOOL)absoluteTouchMode
|
// absoluteTouchMode:(BOOL)absoluteTouchMode
|
||||||
touchMode:(NSInteger)touchMode
|
touchMode:(NSInteger)touchMode
|
||||||
statsOverlayLevel:(NSInteger)statsOverlayLevel
|
statsOverlayLevel:(NSInteger)statsOverlayLevel
|
||||||
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
||||||
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
||||||
resolutionSelected:(NSInteger)resolutionSelected
|
resolutionSelected:(NSInteger)resolutionSelected
|
||||||
externalDisplayMode:(NSInteger)externalDisplayMode
|
externalDisplayMode:(NSInteger)externalDisplayMode
|
||||||
localMousePointerMode:(NSInteger)localMousePointerMode
|
localMousePointerMode:(NSInteger)localMousePointerMode
|
||||||
@@ -95,8 +128,32 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
|
|||||||
enableGraphs:(BOOL)enableGraphs
|
enableGraphs:(BOOL)enableGraphs
|
||||||
graphOpacity:(NSInteger)graphOpacity
|
graphOpacity:(NSInteger)graphOpacity
|
||||||
renderingBackend:(NSInteger)renderingBackend
|
renderingBackend:(NSInteger)renderingBackend
|
||||||
framePacingMode:(NSInteger)framePacingMode
|
framePacingMode:(NSInteger)framePacingMode
|
||||||
backgroundSessionTimer:(NSInteger)backgroundSessionTimer;
|
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;
|
- (NSArray*) getHosts;
|
||||||
- (void) updateHost:(TemporaryHost*)host;
|
- (void) updateHost:(TemporaryHost*)host;
|
||||||
|
|||||||
@@ -55,7 +55,8 @@
|
|||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) saveSettingsWithBitrate:(NSInteger)bitrate
|
- (void) saveSettings:(Settings*)settings
|
||||||
|
withBitrate:(NSInteger)bitrate
|
||||||
framerate:(NSInteger)framerate
|
framerate:(NSInteger)framerate
|
||||||
height:(NSInteger)height
|
height:(NSInteger)height
|
||||||
width:(NSInteger)width
|
width:(NSInteger)width
|
||||||
@@ -71,25 +72,32 @@
|
|||||||
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
|
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
|
||||||
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
|
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
|
||||||
gyroSensitivity:(CGFloat)gyroSensitivity
|
gyroSensitivity:(CGFloat)gyroSensitivity
|
||||||
|
localVolume:(CGFloat)localVolume
|
||||||
|
micVolume:(CGFloat)micVolume
|
||||||
touchMoveEventInterval:(NSInteger)touchMoveEventInterval
|
touchMoveEventInterval:(NSInteger)touchMoveEventInterval
|
||||||
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
|
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
|
||||||
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
|
asyncNativeTouchPriority:(NSInteger)asyncNativeTouchPriority
|
||||||
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
|
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
|
||||||
showKeyboardToolbar:(BOOL)showKeyboardToolbar
|
showKeyboardToolbar:(BOOL)showKeyboardToolbar
|
||||||
optimizeGames:(BOOL)optimizeGames
|
optimizeGames:(BOOL)optimizeGames
|
||||||
multiController:(BOOL)multiController
|
multiController:(BOOL)multiController
|
||||||
|
buttonVisualFeedback:(BOOL)buttonVisualFeedback
|
||||||
|
touchPointTracking:(BOOL)touchPointTracking
|
||||||
swapABXYButtons:(BOOL)swapABXYButtons
|
swapABXYButtons:(BOOL)swapABXYButtons
|
||||||
audioOnPC:(BOOL)audioOnPC
|
audioOnPC:(BOOL)audioOnPC
|
||||||
|
redirectMic:(BOOL)redirectMic
|
||||||
|
useBuiltinMic:(BOOL)useBuiltinMic
|
||||||
preferredCodec:(uint32_t)preferredCodec
|
preferredCodec:(uint32_t)preferredCodec
|
||||||
enableYUV444:(BOOL)enableYUV444
|
enableYUV444:(BOOL)enableYUV444
|
||||||
enablePIP:(BOOL)enablePIP
|
enablePIP:(BOOL)enablePIP
|
||||||
|
fullColorRange:(BOOL)fullColorRange
|
||||||
enableHdr:(BOOL)enableHdr
|
enableHdr:(BOOL)enableHdr
|
||||||
btMouseSupport:(BOOL)btMouseSupport
|
btMouseSupport:(BOOL)btMouseSupport
|
||||||
// absoluteTouchMode:(BOOL)absoluteTouchMode
|
// absoluteTouchMode:(BOOL)absoluteTouchMode
|
||||||
touchMode:(NSInteger)touchMode
|
touchMode:(NSInteger)touchMode
|
||||||
statsOverlayLevel:(NSInteger)statsOverlayLevel
|
statsOverlayLevel:(NSInteger)statsOverlayLevel
|
||||||
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
||||||
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
||||||
resolutionSelected:(NSInteger)resolutionSelected
|
resolutionSelected:(NSInteger)resolutionSelected
|
||||||
externalDisplayMode:(NSInteger)externalDisplayMode
|
externalDisplayMode:(NSInteger)externalDisplayMode
|
||||||
localMousePointerMode:(NSInteger)localMousePointerMode
|
localMousePointerMode:(NSInteger)localMousePointerMode
|
||||||
@@ -97,11 +105,36 @@
|
|||||||
enableGraphs:(BOOL)enableGraphs
|
enableGraphs:(BOOL)enableGraphs
|
||||||
graphOpacity:(NSInteger)graphOpacity
|
graphOpacity:(NSInteger)graphOpacity
|
||||||
renderingBackend:(NSInteger)renderingBackend
|
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{
|
backgroundSessionTimer:(NSInteger)backgroundSessionTimer{
|
||||||
|
|
||||||
|
__block Settings* settingsToSave = settings;
|
||||||
[_managedObjectContext performBlockAndWait:^{
|
[_managedObjectContext performBlockAndWait:^{
|
||||||
Settings* settingsToSave = [self retrieveSettings];
|
if(!settingsToSave) settingsToSave = [self retrieveSettings];
|
||||||
settingsToSave.framerate = [NSNumber numberWithInteger:framerate];
|
settingsToSave.framerate = [NSNumber numberWithInteger:framerate];
|
||||||
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
|
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
|
||||||
settingsToSave.height = [NSNumber numberWithInteger:height];
|
settingsToSave.height = [NSNumber numberWithInteger:height];
|
||||||
@@ -118,6 +151,8 @@
|
|||||||
settingsToSave.touchPointerVelocityFactor = [NSNumber numberWithFloat:touchPointerVelocityFactor];
|
settingsToSave.touchPointerVelocityFactor = [NSNumber numberWithFloat:touchPointerVelocityFactor];
|
||||||
settingsToSave.mousePointerVelocityFactor = [NSNumber numberWithFloat:mousePointerVelocityFactor];
|
settingsToSave.mousePointerVelocityFactor = [NSNumber numberWithFloat:mousePointerVelocityFactor];
|
||||||
settingsToSave.gyroSensitivity = [NSNumber numberWithFloat:gyroSensitivity];
|
settingsToSave.gyroSensitivity = [NSNumber numberWithFloat:gyroSensitivity];
|
||||||
|
settingsToSave.localVolume = [NSNumber numberWithFloat:localVolume];
|
||||||
|
settingsToSave.micVolume = [NSNumber numberWithFloat:micVolume];
|
||||||
settingsToSave.touchMoveEventInterval = [NSNumber numberWithInteger:touchMoveEventInterval];
|
settingsToSave.touchMoveEventInterval = [NSNumber numberWithInteger:touchMoveEventInterval];
|
||||||
settingsToSave.reverseMouseWheelDirection = reverseMouseWheelDirection;
|
settingsToSave.reverseMouseWheelDirection = reverseMouseWheelDirection;
|
||||||
settingsToSave.asyncNativeTouchPriority = [NSNumber numberWithInteger:asyncNativeTouchPriority];
|
settingsToSave.asyncNativeTouchPriority = [NSNumber numberWithInteger:asyncNativeTouchPriority];
|
||||||
@@ -125,11 +160,17 @@
|
|||||||
settingsToSave.showKeyboardToolbar = showKeyboardToolbar;
|
settingsToSave.showKeyboardToolbar = showKeyboardToolbar;
|
||||||
settingsToSave.optimizeGames = optimizeGames;
|
settingsToSave.optimizeGames = optimizeGames;
|
||||||
settingsToSave.multiController = multiController;
|
settingsToSave.multiController = multiController;
|
||||||
|
settingsToSave.buttonVisualFeedback = buttonVisualFeedback;
|
||||||
|
settingsToSave.touchPointTracking = touchPointTracking;
|
||||||
settingsToSave.swapABXYButtons = swapABXYButtons;
|
settingsToSave.swapABXYButtons = swapABXYButtons;
|
||||||
settingsToSave.playAudioOnPC = audioOnPC;
|
settingsToSave.playAudioOnPC = audioOnPC;
|
||||||
|
settingsToSave.redirectMic = redirectMic;
|
||||||
|
settingsToSave.useBuiltinMic = useBuiltinMic;
|
||||||
settingsToSave.preferredCodec = preferredCodec;
|
settingsToSave.preferredCodec = preferredCodec;
|
||||||
settingsToSave.enableYUV444 = enableYUV444;
|
settingsToSave.enableYUV444 = enableYUV444;
|
||||||
|
settingsToSave.sdrPerformanceWorkaround = sdrPerformanceWorkaround;
|
||||||
settingsToSave.enablePIP = enablePIP;
|
settingsToSave.enablePIP = enablePIP;
|
||||||
|
settingsToSave.fullColorRange = fullColorRange;
|
||||||
settingsToSave.enableHdr = enableHdr;
|
settingsToSave.enableHdr = enableHdr;
|
||||||
settingsToSave.btMouseSupport = btMouseSupport;
|
settingsToSave.btMouseSupport = btMouseSupport;
|
||||||
// settingsToSave.absoluteTouchMode = absoluteTouchMode;
|
// settingsToSave.absoluteTouchMode = absoluteTouchMode;
|
||||||
@@ -143,10 +184,32 @@
|
|||||||
settingsToSave.backroundSessionTimer = [NSNumber numberWithInteger:backgroundSessionTimer];
|
settingsToSave.backroundSessionTimer = [NSNumber numberWithInteger:backgroundSessionTimer];
|
||||||
|
|
||||||
settingsToSave.frameQueueSize = [NSNumber numberWithInteger:frameQueueSize];
|
settingsToSave.frameQueueSize = [NSNumber numberWithInteger:frameQueueSize];
|
||||||
|
settingsToSave.enableFrameTimebase = enableFrameTimebase;
|
||||||
|
settingsToSave.asyncFrameDequeue = asyncFrameDequeue;
|
||||||
settingsToSave.enableGraphs = enableGraphs;
|
settingsToSave.enableGraphs = enableGraphs;
|
||||||
settingsToSave.graphOpacity = [NSNumber numberWithInteger:graphOpacity];
|
settingsToSave.graphOpacity = [NSNumber numberWithInteger:graphOpacity];
|
||||||
settingsToSave.renderingBackend = [NSNumber numberWithInteger:renderingBackend];
|
settingsToSave.renderingBackend = [NSNumber numberWithInteger:renderingBackend];
|
||||||
settingsToSave.framePacingMode = [NSNumber numberWithInteger:framePacingMode];
|
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];
|
[self saveData];
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@property (atomic, retain) NSString *name;
|
@property (atomic, retain) NSString *name;
|
||||||
@property (atomic, retain) NSString *uuid;
|
@property (atomic, retain) NSString *uuid;
|
||||||
@property (atomic, retain) NSSet *appList;
|
@property (atomic, retain) NSSet *appList;
|
||||||
|
@property (atomic, retain) NSMutableSet * activeAddressPool;
|
||||||
|
|
||||||
- (id) initFromHost:(Host*)host;
|
- (id) initFromHost:(Host*)host;
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,17 @@
|
|||||||
self = [self init];
|
self = [self init];
|
||||||
|
|
||||||
self.address = host.address;
|
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.externalAddress = host.externalAddress;
|
||||||
self.localAddress = host.localAddress;
|
self.localAddress = host.localAddress;
|
||||||
self.ipv6Address = host.ipv6Address;
|
self.ipv6Address = host.ipv6Address;
|
||||||
@@ -57,7 +68,7 @@
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) propagateChangesToParent:(Host*)parentHost {
|
- (void) propagateChangesToParent:(Host*)parentHost { // update persisted host data
|
||||||
// Avoid overwriting existing data with nil if
|
// Avoid overwriting existing data with nil if
|
||||||
// we don't have everything populated in the temporary
|
// we don't have everything populated in the temporary
|
||||||
// host.
|
// host.
|
||||||
@@ -73,6 +84,15 @@
|
|||||||
if (self.ipv6Address != nil) {
|
if (self.ipv6Address != nil) {
|
||||||
parentHost.ipv6Address = self.ipv6Address;
|
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
|
// try to fix invalid mac happens in some cases
|
||||||
// 添加主机crash问题重点关注
|
// 添加主机crash问题重点关注
|
||||||
if (!(self.mac == nil || [self.mac isEqualToString:@"00:00:00:00:00:00"])) parentHost.mac = self.mac;
|
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 * touchMoveEventInterval;
|
||||||
@property (nonatomic, retain) NSNumber * touchPointerVelocityFactor;
|
@property (nonatomic, retain) NSNumber * touchPointerVelocityFactor;
|
||||||
@property (nonatomic, retain) NSNumber * gyroSensitivity;
|
@property (nonatomic, retain) NSNumber * gyroSensitivity;
|
||||||
|
@property (nonatomic, retain) NSNumber * localVolume;
|
||||||
|
@property (nonatomic, retain) NSNumber * micVolume;
|
||||||
@property (nonatomic, retain) NSNumber * emulatedControllerType;
|
@property (nonatomic, retain) NSNumber * emulatedControllerType;
|
||||||
@property (nonatomic, retain) NSNumber * mousePointerVelocityFactor;
|
@property (nonatomic, retain) NSNumber * mousePointerVelocityFactor;
|
||||||
@property (nonatomic, retain) NSNumber * pointerVelocityModeDivider;
|
@property (nonatomic, retain) NSNumber * pointerVelocityModeDivider;
|
||||||
@@ -40,6 +42,9 @@
|
|||||||
@property (nonatomic, retain) NSNumber * resolutionSelected;
|
@property (nonatomic, retain) NSNumber * resolutionSelected;
|
||||||
@property (nonatomic, retain) NSNumber * externalDisplayMode;
|
@property (nonatomic, retain) NSNumber * externalDisplayMode;
|
||||||
@property (nonatomic, retain) NSNumber * localMousePointerMode;
|
@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, retain) NSNumber * backgroundSessionTimer;
|
||||||
@property (nonatomic) enum {
|
@property (nonatomic) enum {
|
||||||
CODEC_PREF_AUTO,
|
CODEC_PREF_AUTO,
|
||||||
@@ -48,12 +53,18 @@
|
|||||||
CODEC_PREF_AV1,
|
CODEC_PREF_AV1,
|
||||||
} preferredCodec;
|
} preferredCodec;
|
||||||
@property (nonatomic) BOOL enableYUV444;
|
@property (nonatomic) BOOL enableYUV444;
|
||||||
|
@property (nonatomic) BOOL sdrPerformanceWorkaround;
|
||||||
@property (nonatomic) BOOL enablePIP;
|
@property (nonatomic) BOOL enablePIP;
|
||||||
|
@property (nonatomic) BOOL fullColorRange;
|
||||||
@property (nonatomic) BOOL reverseMouseWheelDirection;
|
@property (nonatomic) BOOL reverseMouseWheelDirection;
|
||||||
@property (nonatomic, retain) NSNumber * asyncNativeTouchPriority;
|
@property (nonatomic, retain) NSNumber * asyncNativeTouchPriority;
|
||||||
@property (nonatomic) BOOL multiController;
|
@property (nonatomic) BOOL multiController;
|
||||||
|
@property (nonatomic) BOOL buttonVisualFeedback;
|
||||||
|
@property (nonatomic) BOOL touchPointTracking;
|
||||||
@property (nonatomic) BOOL swapABXYButtons;
|
@property (nonatomic) BOOL swapABXYButtons;
|
||||||
@property (nonatomic) BOOL playAudioOnPC;
|
@property (nonatomic) BOOL playAudioOnPC;
|
||||||
|
@property (nonatomic) BOOL redirectMic;
|
||||||
|
@property (nonatomic) BOOL useBuiltinMic;
|
||||||
@property (nonatomic) BOOL optimizeGames;
|
@property (nonatomic) BOOL optimizeGames;
|
||||||
@property (nonatomic) BOOL enableHdr;
|
@property (nonatomic) BOOL enableHdr;
|
||||||
@property (nonatomic) BOOL btMouseSupport;
|
@property (nonatomic) BOOL btMouseSupport;
|
||||||
@@ -65,10 +76,43 @@
|
|||||||
@property (nonatomic) BOOL showKeyboardToolbar;
|
@property (nonatomic) BOOL showKeyboardToolbar;
|
||||||
@property (nonatomic) BOOL unlockDisplayOrientation;
|
@property (nonatomic) BOOL unlockDisplayOrientation;
|
||||||
@property (nonatomic) BOOL enableGraphs;
|
@property (nonatomic) BOOL enableGraphs;
|
||||||
|
@property (nonatomic) BOOL enableFrameTimebase;
|
||||||
|
@property (nonatomic) BOOL asyncFrameDequeue;
|
||||||
@property (nonatomic, retain) NSNumber * frameQueueSize;
|
@property (nonatomic, retain) NSNumber * frameQueueSize;
|
||||||
@property (nonatomic, retain) NSNumber * graphOpacity;
|
@property (nonatomic, retain) NSNumber * graphOpacity;
|
||||||
@property (nonatomic, retain) NSNumber * renderingBackend;
|
@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 * 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;
|
- (id) initFromSettings:(Settings*)settings;
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"];
|
self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"];
|
||||||
self.enableYUV444 = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableYUV444"];
|
self.enableYUV444 = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableYUV444"];
|
||||||
self.enablePIP = [[NSUserDefaults standardUserDefaults] boolForKey:@"enablePIP"];
|
self.enablePIP = [[NSUserDefaults standardUserDefaults] boolForKey:@"enablePIP"];
|
||||||
|
self.fullRange = [[NSUserDefaults standardUserDefaults] boolForKey:@"fullRange"];
|
||||||
self.frameQueueSize = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"frameQueueSize"]];
|
self.frameQueueSize = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"frameQueueSize"]];
|
||||||
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
|
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
|
||||||
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
|
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
|
||||||
@@ -52,15 +53,7 @@
|
|||||||
self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
|
self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
|
||||||
self.enableGraphs = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableGraphs"];
|
self.enableGraphs = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableGraphs"];
|
||||||
self.graphOpacity = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"graphOpacity"]];
|
self.graphOpacity = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"graphOpacity"]];
|
||||||
// Set rendering backend based on iOS version and user preference
|
self.renderingBackend = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"renderingBackend"]];
|
||||||
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.framePacingMode = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framePacingMode"]];
|
self.framePacingMode = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framePacingMode"]];
|
||||||
|
|
||||||
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
|
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
|
||||||
@@ -95,12 +88,20 @@
|
|||||||
self.audioConfig = settings.audioConfig;
|
self.audioConfig = settings.audioConfig;
|
||||||
self.preferredCodec = settings.preferredCodec;
|
self.preferredCodec = settings.preferredCodec;
|
||||||
self.enableYUV444 = settings.enableYUV444;
|
self.enableYUV444 = settings.enableYUV444;
|
||||||
|
self.sdrPerformanceWorkaround = settings.sdrPerformanceWorkaround;
|
||||||
self.enablePIP = settings.enablePIP;
|
self.enablePIP = settings.enablePIP;
|
||||||
|
self.fullColorRange = settings.fullColorRange;
|
||||||
self.frameQueueSize = settings.frameQueueSize;
|
self.frameQueueSize = settings.frameQueueSize;
|
||||||
|
self.enableFrameTimebase = settings.enableFrameTimebase;
|
||||||
|
self.asyncFrameDequeue = settings.asyncFrameDequeue;
|
||||||
self.playAudioOnPC = settings.playAudioOnPC;
|
self.playAudioOnPC = settings.playAudioOnPC;
|
||||||
|
self.redirectMic = settings.redirectMic;
|
||||||
|
self.useBuiltinMic = settings.useBuiltinMic;
|
||||||
self.enableHdr = settings.enableHdr;
|
self.enableHdr = settings.enableHdr;
|
||||||
self.optimizeGames = settings.optimizeGames;
|
self.optimizeGames = settings.optimizeGames;
|
||||||
self.multiController = settings.multiController;
|
self.multiController = settings.multiController;
|
||||||
|
self.buttonVisualFeedback = settings.buttonVisualFeedback;
|
||||||
|
self.touchPointTracking = settings.touchPointTracking;
|
||||||
self.swapABXYButtons = settings.swapABXYButtons;
|
self.swapABXYButtons = settings.swapABXYButtons;
|
||||||
self.onscreenControls = settings.onscreenControls;
|
self.onscreenControls = settings.onscreenControls;
|
||||||
self.gyroMode = settings.gyroMode;
|
self.gyroMode = settings.gyroMode;
|
||||||
@@ -122,6 +123,8 @@
|
|||||||
self.touchPointerVelocityFactor = settings.touchPointerVelocityFactor;
|
self.touchPointerVelocityFactor = settings.touchPointerVelocityFactor;
|
||||||
self.mousePointerVelocityFactor = settings.mousePointerVelocityFactor;
|
self.mousePointerVelocityFactor = settings.mousePointerVelocityFactor;
|
||||||
self.gyroSensitivity = settings.gyroSensitivity;
|
self.gyroSensitivity = settings.gyroSensitivity;
|
||||||
|
self.localVolume = settings.localVolume;
|
||||||
|
self.micVolume = settings.micVolume;
|
||||||
self.pointerVelocityModeDivider = settings.pointerVelocityModeDivider;
|
self.pointerVelocityModeDivider = settings.pointerVelocityModeDivider;
|
||||||
self.unlockDisplayOrientation = settings.unlockDisplayOrientation;
|
self.unlockDisplayOrientation = settings.unlockDisplayOrientation;
|
||||||
self.resolutionSelected = settings.resolutionSelected;
|
self.resolutionSelected = settings.resolutionSelected;
|
||||||
@@ -131,9 +134,43 @@
|
|||||||
self.graphOpacity = settings.graphOpacity;
|
self.graphOpacity = settings.graphOpacity;
|
||||||
self.renderingBackend = settings.renderingBackend;
|
self.renderingBackend = settings.renderingBackend;
|
||||||
self.framePacingMode = settings.framePacingMode;
|
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;
|
self.backgroundSessionTimer = settings.backroundSessionTimer;
|
||||||
#endif
|
self.edgeSlidingSensitivity = settings.edgeSlidingSensitivity;
|
||||||
|
self.appTheme = settings.appTheme;
|
||||||
|
self.hapticEngine = settings.hapticEngine;
|
||||||
self.uniqueId = settings.uniqueId;
|
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;
|
return self;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,6 @@
|
|||||||
- (void)resizeBySizeFactor:(CGFloat)factor;
|
- (void)resizeBySizeFactor:(CGFloat)factor;
|
||||||
- (id) initWithHost:(TemporaryHost*)host;
|
- (id) initWithHost:(TemporaryHost*)host;
|
||||||
- (id) initWithHost:(TemporaryHost*)host andSizeFactor:(CGFloat)sizeFactor;
|
- (id) initWithHost:(TemporaryHost*)host andSizeFactor:(CGFloat)sizeFactor;
|
||||||
|
- (void)updateTheme;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
#import "HostCardView.h"
|
#import "HostCardView.h"
|
||||||
#import "LocalizationHelper.h"
|
#import "LocalizationHelper.h"
|
||||||
#import "ThemeManager.h"
|
#import "VoidLink-Swift.h"
|
||||||
#import "FixedTintImageView.h"
|
#import "FixedTintImageView.h"
|
||||||
|
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
|||||||
computerIconMonitorCenterYOffset = [self isIPhone] ? -2.75*_sizeFactor : -3.2*_sizeFactor;
|
computerIconMonitorCenterYOffset = [self isIPhone] ? -2.75*_sizeFactor : -3.2*_sizeFactor;
|
||||||
iconAndButtonSpacing = 37*_sizeFactor;
|
iconAndButtonSpacing = 37*_sizeFactor;
|
||||||
buttonHeight = 39*_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];
|
defaultGreen = [UIColor colorWithRed:52.0/255.0 green:199.0/255.0 blue:89.0/255.0 alpha:1.0];
|
||||||
// self.userIterfaceStyle = UIUserInterfaceStyleLight
|
// self.userIterfaceStyle = UIUserInterfaceStyleLight
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
|||||||
|
|
||||||
- (void)setupUI {
|
- (void)setupUI {
|
||||||
self.userInteractionEnabled = YES;
|
self.userInteractionEnabled = YES;
|
||||||
self.backgroundColor = [ThemeManager widgetBackgroundColor]; // theme
|
self.backgroundColor = ThemeManager.widgetBackgroundColor; // theme
|
||||||
self.cardContentpadding = 13 * _sizeFactor;
|
self.cardContentpadding = 13 * _sizeFactor;
|
||||||
self.clipsToBounds = YES;
|
self.clipsToBounds = YES;
|
||||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
@@ -296,7 +296,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
|||||||
self.appButton.translatesAutoresizingMaskIntoConstraints = NO;
|
self.appButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
self.appButton.frame = CGRectMake(20, 200, 150, buttonHeight);
|
self.appButton.frame = CGRectMake(20, 200, 150, buttonHeight);
|
||||||
[self.appButton setTitle:[LocalizationHelper localizedStringForKey:@"Applications"] forState:UIControlStateNormal];
|
[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.titleLabel.font = [UIFont systemFontOfSize:buttonLabelFontSize];
|
||||||
[self.appButton addTarget:self action:@selector(appButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
[self.appButton addTarget:self action:@selector(appButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||||
[self addSubview:self.appButton];
|
[self addSubview:self.appButton];
|
||||||
@@ -325,7 +325,6 @@ static const float REFRESH_CYCLE = 2.0f;
|
|||||||
if (@available(iOS 13.0, *)) {
|
if (@available(iOS 13.0, *)) {
|
||||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight*0.263];
|
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight*0.263];
|
||||||
[self.launchButton setImage:[UIImage systemImageNamed:@"play.fill" withConfiguration:config] forState:UIControlStateNormal];
|
[self.launchButton setImage:[UIImage systemImageNamed:@"play.fill" withConfiguration:config] forState:UIControlStateNormal];
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback on earlier versions
|
// Fallback on earlier versions
|
||||||
}
|
}
|
||||||
@@ -343,7 +342,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
|||||||
self.pairButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
self.pairButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||||
self.pairButton.translatesAutoresizingMaskIntoConstraints = NO;
|
self.pairButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
self.pairButton.frame = CGRectMake(0, 0, 150, 50);
|
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.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0377/2);
|
||||||
[self.pairButton setTitle:[LocalizationHelper localizedStringForKey:@" Pair with PIN"] forState:UIControlStateNormal];
|
[self.pairButton setTitle:[LocalizationHelper localizedStringForKey:@" Pair with PIN"] forState:UIControlStateNormal];
|
||||||
[self.pairButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
[self.pairButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
||||||
@@ -356,7 +355,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
|||||||
} else {
|
} else {
|
||||||
// Fallback on earlier versions
|
// Fallback on earlier versions
|
||||||
}
|
}
|
||||||
self.pairButton.backgroundColor = [ThemeManager textTintColorWithAlpha];
|
self.pairButton.backgroundColor = ThemeManager.textTintColorWithAlpha;
|
||||||
[self.pairButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
[self.pairButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
||||||
|
|
||||||
[self.pairButton addTarget:self action:@selector(pairButtonTapped) forControlEvents:UIControlEventPrimaryActionTriggered];
|
[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 = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||||
self.wakeupButton.translatesAutoresizingMaskIntoConstraints = NO;
|
self.wakeupButton.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
self.wakeupButton.frame = CGRectMake(0, 0, 150, 50);
|
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.layer.cornerRadius = 2*(uint16_t)(cardWidth*0.0377/2);
|
||||||
[self.wakeupButton setTitle:[LocalizationHelper localizedStringForKey:@" Wake-on-LAN"] forState:UIControlStateNormal];
|
[self.wakeupButton setTitle:[LocalizationHelper localizedStringForKey:@" Wake-on-LAN"] forState:UIControlStateNormal];
|
||||||
[self.wakeupButton setTitleColor:defaultBlue forState:UIControlStateNormal]; // theme
|
[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],
|
[self.transparentButton.bottomAnchor constraintEqualToAnchor:self.separatorLine.topAnchor],
|
||||||
]];
|
]];
|
||||||
|
|
||||||
|
|
||||||
_widthConstraint.constant = _cardContentpadding*2 + appButtonWidth + 15*_sizeFactor + launchButtonWidth;
|
_widthConstraint.constant = _cardContentpadding*2 + appButtonWidth + 15*_sizeFactor + launchButtonWidth;
|
||||||
_heightConstraint.constant = _cardContentpadding*2 + _iconBackgroundView.frame.size.height + iconAndButtonSpacing + buttonHeight + 1;
|
_heightConstraint.constant = _cardContentpadding*2 + _iconBackgroundView.frame.size.height + iconAndButtonSpacing + buttonHeight + 1;
|
||||||
|
|
||||||
_size = CGSizeMake(_widthConstraint.constant, _heightConstraint.constant);
|
_size = CGSizeMake(_widthConstraint.constant, _heightConstraint.constant);
|
||||||
|
|
||||||
[self updateTheme:ThemeManager.userInterfaceStyle];
|
[self updateTheme];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)createBackgroundLayer{
|
- (void)updateBackgroundLayerTheme{
|
||||||
backgroundLayer = [CAGradientLayer layer];
|
|
||||||
UIColor *gradientColorDark = [UIColor colorWithRed:0.0 green:0.319 blue:0.64 alpha:1.0];
|
UIColor *gradientColorDark = [UIColor colorWithRed:0.0 green:0.319 blue:0.64 alpha:1.0];
|
||||||
UIColor *gradientColorLight = [gradientColorDark colorWithAlphaComponent:0.52];
|
UIColor *gradientColorLight = [gradientColorDark colorWithAlphaComponent:0.52];
|
||||||
CGColorRef gradientColorRef = ThemeManager.userInterfaceStyle == UIUserInterfaceStyleDark ? gradientColorDark.CGColor : gradientColorLight.CGColor;
|
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)[UIColor clearColor].CGColor,
|
||||||
(__bridge id)gradientColorRef
|
(__bridge id)gradientColorRef
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)createBackgroundLayer{
|
||||||
|
backgroundLayer = [CAGradientLayer layer];
|
||||||
|
[self updateBackgroundLayerTheme];
|
||||||
backgroundLayer.locations = @[@0, @0.18, @0.5, @1];
|
backgroundLayer.locations = @[@0, @0.18, @0.5, @1];
|
||||||
backgroundLayer.startPoint = CGPointMake(0.25, 0.5);
|
backgroundLayer.startPoint = CGPointMake(0.25, 0.5);
|
||||||
backgroundLayer.endPoint = CGPointMake(0.75, 0.5);
|
backgroundLayer.endPoint = CGPointMake(0.75, 0.5);
|
||||||
@@ -469,12 +470,15 @@ static const float REFRESH_CYCLE = 2.0f;
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void)updateTheme:(UIUserInterfaceStyle)userIterfaceStyle{
|
- (void)updateTheme {
|
||||||
self.backgroundColor = [ThemeManager widgetBackgroundColor];
|
self.backgroundColor = ThemeManager.widgetBackgroundColor;
|
||||||
_hostNameLabel.textColor = [ThemeManager textColor];
|
_hostNameLabel.textColor = ThemeManager.textColor;
|
||||||
[_appButton setTitleColor:[ThemeManager appPrimaryColor] forState:UIControlStateNormal];
|
[_appButton setTitleColor:ThemeManager.appPrimaryColor forState:UIControlStateNormal];
|
||||||
_separatorLine.backgroundColor = [ThemeManager separatorColor];
|
_separatorLine.backgroundColor = ThemeManager.hostCardSeparatorColor;
|
||||||
|
|
||||||
backgroundLayer.hidden = NO;
|
backgroundLayer.hidden = NO;
|
||||||
|
[self updateBackgroundLayerTheme];
|
||||||
|
|
||||||
[self updateContentsForHost:_host];
|
[self updateContentsForHost:_host];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,7 +504,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (bool)isDarkTheme{
|
- (bool)isDarkTheme{
|
||||||
return self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark;
|
return ThemeManager.userInterfaceStyle == UIUserInterfaceStyleDark;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) updateLoop {
|
- (void) updateLoop {
|
||||||
@@ -555,7 +559,7 @@ static const float REFRESH_CYCLE = 2.0f;
|
|||||||
[_launchButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // theme
|
[_launchButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // theme
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
_iconBackgroundView.backgroundColor = [ThemeManager appPrimaryColorWithAlpha];
|
_iconBackgroundView.backgroundColor = ThemeManager.appPrimaryColorWithAlpha;
|
||||||
UIColor* iconColor = [[UIColor whiteColor] colorWithAlphaComponent:[self isDarkTheme] ? 0.63 : 1];
|
UIColor* iconColor = [[UIColor whiteColor] colorWithAlphaComponent:[self isDarkTheme] ? 0.63 : 1];
|
||||||
_hostIconView.tintColor = iconColor;
|
_hostIconView.tintColor = iconColor;
|
||||||
lockIconView.tintColor = iconColor;
|
lockIconView.tintColor = iconColor;
|
||||||
@@ -569,18 +573,18 @@ static const float REFRESH_CYCLE = 2.0f;
|
|||||||
case StateOffline:
|
case StateOffline:
|
||||||
[_hostSpinner stopAnimating];
|
[_hostSpinner stopAnimating];
|
||||||
// _iconBackgroundView.backgroundColor = _userIterfaceStyle == UIUserInterfaceStyleDark ? [UIColor appBackgroundColorDark] : [UIColor appBackgroundColorLight];
|
// _iconBackgroundView.backgroundColor = _userIterfaceStyle == UIUserInterfaceStyleDark ? [UIColor appBackgroundColorDark] : [UIColor appBackgroundColorLight];
|
||||||
_iconBackgroundView.backgroundColor = [ThemeManager appBackgroundColor];
|
_iconBackgroundView.backgroundColor = ThemeManager.offlineHostIconBackgroundColor;
|
||||||
_statusLabel.textColor = [ThemeManager textColorGray];
|
_statusLabel.textColor = ThemeManager.textColorGray;
|
||||||
_statusLabel.text = [LocalizationHelper localizedStringForKey: @"Offline"];
|
_statusLabel.text = [LocalizationHelper localizedStringForKey: @"Offline"];
|
||||||
_statusIcon.tintColor = [ThemeManager textColorGray];
|
_statusIcon.tintColor = ThemeManager.textColorGray;
|
||||||
if (@available(iOS 13.0, *)) {
|
if (@available(iOS 13.0, *)) {
|
||||||
_statusIcon.image = [UIImage systemImageNamed:@"exclamationmark.triangle.fill"];
|
_statusIcon.image = [UIImage systemImageNamed:@"exclamationmark.triangle.fill"];
|
||||||
} else {
|
} else {
|
||||||
self.statusIcon.hidden = YES;
|
self.statusIcon.hidden = YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
_hostIconView.tintColor = [ThemeManager lowProfileGray];
|
_hostIconView.tintColor = ThemeManager.lowProfileGray;
|
||||||
lockIconView.tintColor = [ThemeManager lowProfileGray];
|
lockIconView.tintColor = ThemeManager.lowProfileGray;
|
||||||
lockIconView.hidden = hostPaired;
|
lockIconView.hidden = hostPaired;
|
||||||
_appButton.hidden = YES;
|
_appButton.hidden = YES;
|
||||||
_launchButton.hidden = YES;
|
_launchButton.hidden = YES;
|
||||||
@@ -590,24 +594,24 @@ static const float REFRESH_CYCLE = 2.0f;
|
|||||||
if (@available(iOS 13.0, *)) {
|
if (@available(iOS 13.0, *)) {
|
||||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.45 weight:UIImageSymbolWeightBold];
|
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.45 weight:UIImageSymbolWeightBold];
|
||||||
UIImage *templateImage = [UIImage systemImageNamed:@"power" withConfiguration:config];
|
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];
|
[self.wakeupButton setImage:coloredImage forState:UIControlStateNormal];
|
||||||
} else {
|
} else {
|
||||||
// Fallback on earlier versions
|
// Fallback on earlier versions
|
||||||
}
|
}
|
||||||
|
|
||||||
self.wakeupButton.backgroundColor = hasValidMac ? [ThemeManager textTintColorWithAlpha] : [[ThemeManager textColorGray] colorWithAlphaComponent:0.2];
|
self.wakeupButton.backgroundColor = hasValidMac ? ThemeManager.textTintColorWithAlpha : [ThemeManager.textColorGray colorWithAlphaComponent:0.2];
|
||||||
[self.wakeupButton setTitleColor: hasValidMac ? defaultBlue : [ThemeManager textColorGray] forState:UIControlStateNormal];
|
[self.wakeupButton setTitleColor: hasValidMac ? defaultBlue : ThemeManager.textColorGray forState:UIControlStateNormal];
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case StateUnknown:
|
case StateUnknown:
|
||||||
_hostSpinner.color = [UIColor whiteColor];
|
_hostSpinner.color = [UIColor whiteColor];
|
||||||
[_hostSpinner startAnimating];
|
[_hostSpinner startAnimating];
|
||||||
// _iconBackgroundView.backgroundColor = _userIterfaceStyle == UIUserInterfaceStyleDark ? [UIColor appBackgroundColorDark] : [UIColor appBackgroundColorLight];
|
// _iconBackgroundView.backgroundColor = _userIterfaceStyle == UIUserInterfaceStyleDark ? [UIColor appBackgroundColorDark] : [UIColor appBackgroundColorLight];
|
||||||
_iconBackgroundView.backgroundColor = [ThemeManager appPrimaryColorWithAlpha];
|
_iconBackgroundView.backgroundColor = ThemeManager.appPrimaryColorWithAlpha;
|
||||||
_statusLabel.textColor = [ThemeManager textColorGray];
|
_statusLabel.textColor = ThemeManager.textColorGray;
|
||||||
_statusLabel.text = [LocalizationHelper localizedStringForKey: @"Detecting..."];
|
_statusLabel.text = [LocalizationHelper localizedStringForKey: @"Detecting..."];
|
||||||
_statusIcon.tintColor = [ThemeManager textColorGray];
|
_statusIcon.tintColor = ThemeManager.textColorGray;
|
||||||
if (@available(iOS 13.0, *)) {
|
if (@available(iOS 13.0, *)) {
|
||||||
_statusIcon.image = [UIImage systemImageNamed:@"antenna.radiowaves.left.and.right"];
|
_statusIcon.image = [UIImage systemImageNamed:@"antenna.radiowaves.left.and.right"];
|
||||||
} else {
|
} else {
|
||||||
@@ -624,13 +628,13 @@ static const float REFRESH_CYCLE = 2.0f;
|
|||||||
if (@available(iOS 13.0, *)) {
|
if (@available(iOS 13.0, *)) {
|
||||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.45 weight:UIImageSymbolWeightBold];
|
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:buttonHeight/3.45 weight:UIImageSymbolWeightBold];
|
||||||
UIImage *templateImage = [UIImage systemImageNamed:@"power" withConfiguration:config];
|
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];
|
[self.wakeupButton setImage:coloredImage forState:UIControlStateNormal];
|
||||||
} else {
|
} else {
|
||||||
// Fallback on earlier versions
|
// Fallback on earlier versions
|
||||||
}
|
}
|
||||||
self.wakeupButton.backgroundColor = hasValidMac ? [ThemeManager textTintColorWithAlpha] : [[ThemeManager textColorGray] colorWithAlphaComponent:0.2];
|
self.wakeupButton.backgroundColor = hasValidMac ? ThemeManager.textTintColorWithAlpha : [ThemeManager.textColorGray colorWithAlphaComponent:0.2];
|
||||||
[self.wakeupButton setTitleColor: hasValidMac ? defaultBlue : [ThemeManager textColorGray] forState:UIControlStateNormal];
|
[self.wakeupButton setTitleColor: hasValidMac ? defaultBlue : ThemeManager.textColorGray forState:UIControlStateNormal];
|
||||||
break;
|
break;
|
||||||
default: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
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@interface AbsoluteTouchHandler : UIResponder
|
@interface AbsoluteTouchHandler : UIResponder
|
||||||
|
@property (class, nonatomic, assign) int mouseButtonForCursorMove;
|
||||||
|
|
||||||
-(id)initWithView:(StreamView*)view;
|
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
||||||
|
- (void)pauseLeftButtonDrag;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "AbsoluteTouchHandler.h"
|
#import "AbsoluteTouchHandler.h"
|
||||||
|
#import "VoidLink-Swift.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
#define LONG_PRESS_ACTIVATION_DELAY 0.650f
|
#define LONG_PRESS_ACTIVATION_DELAY 0.650f
|
||||||
|
|
||||||
// How far the finger can move before it cancels a right click
|
// 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
|
// How long the double tap deadzone stays in effect between touch up and touch down
|
||||||
#define DOUBLE_TAP_DEAD_ZONE_DELAY 0.250f
|
#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
|
// How far the finger can move before it can override the double tap deadzone
|
||||||
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
|
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
|
||||||
|
|
||||||
|
static int mouseButtonForCursorMove = BUTTON_LEFT;
|
||||||
|
|
||||||
@implementation AbsoluteTouchHandler {
|
@implementation AbsoluteTouchHandler {
|
||||||
StreamView* streamView;
|
__weak StreamView* streamView;
|
||||||
|
|
||||||
|
bool multiTouchesDetected;
|
||||||
|
bool passthroughGestures;
|
||||||
|
|
||||||
NSTimer* longPressTimer;
|
NSTimer* longPressTimer;
|
||||||
UITouch* lastTouchDown;
|
UITouch* lastTouchDown;
|
||||||
@@ -34,37 +40,83 @@
|
|||||||
UITouch* lastTouchUp;
|
UITouch* lastTouchUp;
|
||||||
CGPoint lastTouchUpLocation;
|
CGPoint lastTouchUpLocation;
|
||||||
|
|
||||||
|
UITouch* capturedTouch;
|
||||||
|
|
||||||
|
CGPoint touchBeganLocation;
|
||||||
|
CGPoint movingTouchLocation;
|
||||||
|
|
||||||
|
NSTimeInterval touchBeganTimeStamp;
|
||||||
|
NSTimeInterval leftClickTimeThreshold;
|
||||||
|
|
||||||
// upper screen edge check
|
// upper screen edge check
|
||||||
bool touchPointSpawnedAtUpperScreenEdge;
|
bool touchPointSpawnedAtUpperScreenEdge;
|
||||||
CGFloat slideGestureVerticalThreshold;
|
CGFloat slideGestureVerticalThreshold;
|
||||||
CGFloat screenWidthWithThreshold;
|
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 = [self init];
|
||||||
self->streamView = view;
|
self->streamView = view;
|
||||||
|
|
||||||
|
multiTouchesDetected = false;
|
||||||
|
passthroughGestures = settings.passthroughGestures;
|
||||||
|
|
||||||
|
_delayMouseLeftClick = settings.delayLeftClick;
|
||||||
|
// _delayMouseLeftClick = true;
|
||||||
|
dragButtonDown = false;
|
||||||
|
|
||||||
|
leftClickTimeThreshold = 0.1;
|
||||||
|
|
||||||
// upper screen check
|
// upper screen check
|
||||||
EDGE_TOLERANCE = 10.0;
|
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
|
||||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
||||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
|
||||||
self->touchPointSpawnedAtUpperScreenEdge = false;
|
self->touchPointSpawnedAtUpperScreenEdge = false;
|
||||||
|
|
||||||
|
leftClickDelay = ((CGFloat)settings.leftClickDelayMs.intValue)/1000;
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)onLongPressStart:(NSTimer*)timer {
|
- (void)onLongPressStart:(NSTimer*)timer {
|
||||||
// Raise the left click and start a right click
|
// Raise the left click and start a right click
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
if(multiTouchesDetected) return;
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
|
||||||
|
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 {
|
- (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];
|
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;
|
self->touchPointSpawnedAtUpperScreenEdge = true;
|
||||||
return; // we're done here. this touch event will not be sent to the remote PC.
|
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.
|
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
|
// Ignore touch down events with more than one finger
|
||||||
|
/*
|
||||||
if ([[event allTouches] count] > 1) {
|
if ([[event allTouches] count] > 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
UITouch* touch = [touches anyObject];
|
capturedTouch = [touches anyObject];
|
||||||
CGPoint touchLocation = [touch locationInView:streamView];
|
CGPoint touchLocation = [capturedTouch locationInView:streamView];
|
||||||
|
|
||||||
|
touchBeganTimeStamp = capturedTouch.timestamp;
|
||||||
|
|
||||||
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
|
// 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) +
|
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) {
|
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
|
// 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
|
// Start the long press timer
|
||||||
longPressTimer = [NSTimer scheduledTimerWithTimeInterval:LONG_PRESS_ACTIVATION_DELAY
|
longPressTimer = [NSTimer scheduledTimerWithTimeInterval:LONG_PRESS_ACTIVATION_DELAY
|
||||||
@@ -96,8 +153,21 @@
|
|||||||
userInfo:nil
|
userInfo:nil
|
||||||
repeats:NO];
|
repeats:NO];
|
||||||
|
|
||||||
lastTouchDown = touch;
|
lastTouchDown = capturedTouch;
|
||||||
lastTouchDownLocation = touchLocation;
|
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 {
|
- (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.
|
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
|
// Ignore touch move events with more than one finger
|
||||||
|
/*
|
||||||
if ([[event allTouches] count] > 1) {
|
if ([[event allTouches] count] > 1) {
|
||||||
return;
|
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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(currentTouchesCount > 1) return;
|
||||||
|
|
||||||
UITouch* touch = [touches anyObject];
|
if(![currentTouches containsObject:capturedTouch]) return;
|
||||||
CGPoint touchLocation = [touch locationInView:streamView];
|
|
||||||
|
|
||||||
if (sqrt(pow((touchLocation.x / streamView.bounds.size.width) - (lastTouchDownLocation.x / streamView.bounds.size.width), 2) +
|
movingTouchLocation = [capturedTouch locationInView:streamView];
|
||||||
pow((touchLocation.y / streamView.bounds.size.height) - (lastTouchDownLocation.y / streamView.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) {
|
|
||||||
|
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.
|
// Moved too far since touch down. Cancel the long press timer.
|
||||||
[longPressTimer invalidate];
|
[longPressTimer invalidate];
|
||||||
longPressTimer = nil;
|
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 {
|
- (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(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
|
// Only fire this logic if all touches have ended
|
||||||
if ([[event allTouches] count] == [touches count]) {
|
if ([touches containsObject:capturedTouch]) {
|
||||||
// Cancel the long press timer
|
// Cancel the long press timer
|
||||||
[longPressTimer invalidate];
|
[longPressTimer invalidate];
|
||||||
longPressTimer = nil;
|
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
|
// 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];
|
lastTouchUp = [touches anyObject];
|
||||||
lastTouchUpLocation = [lastTouchUp locationInView:streamView];
|
lastTouchUpLocation = [lastTouchUp locationInView:streamView];
|
||||||
|
|
||||||
|
dragButtonDown = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,4 +267,34 @@
|
|||||||
[self touchesEnded:touches withEvent:event];
|
[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
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ import UIKit
|
|||||||
"M_X1" : BUTTON_X1,
|
"M_X1" : BUTTON_X1,
|
||||||
"MX1" : BUTTON_X1,
|
"MX1" : BUTTON_X1,
|
||||||
"M_X2" : BUTTON_X2,
|
"M_X2" : BUTTON_X2,
|
||||||
"MX2" : BUTTON_X2
|
"MX2" : BUTTON_X2,
|
||||||
|
"WHEELUP" : 0xFF,
|
||||||
|
"WHEELDOWN" : -0xFF,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -96,6 +98,7 @@ import UIKit
|
|||||||
"PADDLE3" : PADDLE3_FLAG,
|
"PADDLE3" : PADDLE3_FLAG,
|
||||||
"PADDLE4" : PADDLE4_FLAG,
|
"PADDLE4" : PADDLE4_FLAG,
|
||||||
"MISC" : MISC_FLAG,
|
"MISC" : MISC_FLAG,
|
||||||
|
"OSCHOME" : SPECIAL_FLAG,
|
||||||
"OSCL2" : 0,
|
"OSCL2" : 0,
|
||||||
"L2" : 0,
|
"L2" : 0,
|
||||||
"LT" : 0,
|
"LT" : 0,
|
||||||
@@ -115,16 +118,49 @@ import UIKit
|
|||||||
"OSCBACK"
|
"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 stickTouchPads: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD"]
|
||||||
@objc public static let nonVectorStickPads: [String] = ["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"]
|
// @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
|
// Windows Key Codes
|
||||||
"NULL": 0xFF,
|
"NULL": 0xFF,
|
||||||
|
"BRUSH": 0xFF,
|
||||||
|
"ERASER": 0xFF,
|
||||||
"CTRL": 0x11, // VK_CONTROL
|
"CTRL": 0x11, // VK_CONTROL
|
||||||
"SHIFT": 0x10, // VK_SHIFT
|
"SHIFT": 0x10, // VK_SHIFT
|
||||||
"ALT": 0x12, // VK_MENU
|
"ALT": 0x12, // VK_MENU
|
||||||
@@ -383,7 +419,7 @@ import UIKit
|
|||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
//if true { // save default entries if the data is empty.
|
//if true { // save default entries if the data is empty.
|
||||||
if defaults.data(forKey: "savedCommands") == nil { // 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: "WIN", alias: "WIN"),
|
||||||
RemoteCommand(cmdString: "F11", alias: "F11"),
|
RemoteCommand(cmdString: "F11", alias: "F11"),
|
||||||
RemoteCommand(cmdString: "ESC", alias: "ESC"),
|
RemoteCommand(cmdString: "ESC", alias: "ESC"),
|
||||||
@@ -410,22 +446,25 @@ import UIKit
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extractKeyStrings from keyboardCMDString
|
// extractKeyStrings from keyboardCMDString
|
||||||
@objc public func extractKeyStringsFromComboCommand(from input: String) -> [String]? {
|
@objc public func extractAutoReleaseButtonStrings(from cmd: String) -> [String]? {
|
||||||
let keys = CommandManager.keyboardButtonMappings.keys.joined(separator: "|")
|
let cmd = cmd.uppercased()
|
||||||
|
let mergedKeys = (Set(CommandManager.keyboardButtonMappings.keys)
|
||||||
|
.union(Set(CommandManager.mouseButtonMappings.keys)))
|
||||||
|
let keys = mergedKeys.joined(separator: "|")
|
||||||
let pattern = "^(?:(\(keys))(?:\\+(\(keys))*)*)$"
|
let pattern = "^(?:(\(keys))(?:\\+(\(keys))*)*)$"
|
||||||
|
|
||||||
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
||||||
print("Failed to create regex")
|
print("Failed to create regex")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let range = NSRange(location: 0, length: input.utf16.count)
|
let range = NSRange(location: 0, length: cmd.utf16.count)
|
||||||
guard let match = regex.firstMatch(in: input, options: [], range: range) else {
|
guard let match = regex.firstMatch(in: cmd, options: [], range: range) else {
|
||||||
print("No match found for input: \(input)")
|
print("No match found for input: \(cmd)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// print("Regex matched for input: \(input)")
|
// 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) }
|
let keyStrings = matchedString.split(separator: "+").map { String($0) }
|
||||||
|
|
||||||
guard !keyStrings.isEmpty else {
|
guard !keyStrings.isEmpty else {
|
||||||
@@ -436,7 +475,8 @@ import UIKit
|
|||||||
var validKeyStrings: [String] = []
|
var validKeyStrings: [String] = []
|
||||||
|
|
||||||
for key in keyStrings {
|
for key in keyStrings {
|
||||||
if CommandManager.keyboardButtonMappings.keys.contains(key) {
|
if (CommandManager.keyboardButtonMappings.keys.contains(key)
|
||||||
|
|| CommandManager.mouseButtonMappings.keys.contains(key)) {
|
||||||
validKeyStrings.append(key)
|
validKeyStrings.append(key)
|
||||||
} else {
|
} else {
|
||||||
print(" '\(key)' is not defined in key mappings")
|
print(" '\(key)' is not defined in key mappings")
|
||||||
@@ -458,10 +498,13 @@ import UIKit
|
|||||||
}
|
}
|
||||||
|
|
||||||
//super combo key button strings
|
//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 },
|
let combinedStrings = [CommandManager.keyboardButtonMappings.keys.map { $0 as String },
|
||||||
CommandManager.oscButtonMappings.keys.map { $0 as String },
|
CommandManager.oscButtonMappings.keys.map { $0 as String },
|
||||||
CommandManager.mouseButtonMappings.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 }
|
CommandManager.touchPadCmds.map { $0 as String }
|
||||||
]
|
]
|
||||||
.lazy
|
.lazy
|
||||||
@@ -512,7 +555,7 @@ import UIKit
|
|||||||
@objc public func addCommand(_ command: RemoteCommand) -> Bool {
|
@objc public func addCommand(_ command: RemoteCommand) -> Bool {
|
||||||
command.cmdString = command.cmdString.uppercased() // convert all letters to upper case
|
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
|
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
|
if (keyStrings == nil) {return false} // in case of non-keyboard command strings, return false
|
||||||
commands.append(command)
|
commands.append(command)
|
||||||
saveCommands()
|
saveCommands()
|
||||||
@@ -551,38 +594,55 @@ import UIKit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func saveCommands() {
|
private func saveCommands() {
|
||||||
if let data = try? NSKeyedArchiver.archivedData(withRootObject: commands, requiringSecureCoding: false) {
|
if let data = try? NSKeyedArchiver.archivedData(withRootObject: commands, requiringSecureCoding: false) {
|
||||||
UserDefaults.standard.set(data, forKey: "savedCommands")
|
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 {
|
guard let cmdStrings = cmdStrings else { return }
|
||||||
// 释放按键
|
|
||||||
for keyStr in keyboardCmdStrings.reversed() { // 从后往前释放按键
|
if releaseOnly {
|
||||||
|
for keyStr in cmdStrings.reversed() {
|
||||||
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
|
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
|
||||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_UP), 0) // 释放按键
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前按键的映射值
|
// 获取当前按键的映射值
|
||||||
if let keyCode = CommandManager.keyboardButtonMappings[keyboardCmdStrings[index]] {
|
if let keyCode = CommandManager.keyboardButtonMappings[cmdStrings[index]] {
|
||||||
// 发送当前按键的按下事件
|
// 发送当前按键的按下事件
|
||||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_DOWN), 0)
|
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_DOWN), 0)
|
||||||
|
} else if let mouseButtonCode = CommandManager.mouseButtonMappings[cmdStrings[index]]{
|
||||||
// 延迟后递归处理下一个按键
|
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), mouseButtonCode)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
|
||||||
self.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index + 1)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
print("No mapping found for \(keyboardCmdStrings[index])")
|
print("No mapping found for \(cmdStrings[index])")
|
||||||
// 如果当前按键没有映射,跳过当前按键并继续下一个
|
}
|
||||||
self.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index + 1)
|
// 延迟后递归处理下一个按键
|
||||||
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
#import "ControllerSupport.h"
|
#import "ControllerSupport.h"
|
||||||
#import "VoidController.h"
|
#import "VoidController.h"
|
||||||
|
#import "VoidLink-Swift.h"
|
||||||
#import "OnScreenControls.h"
|
#import "OnScreenControls.h"
|
||||||
|
|
||||||
#import "DataManager.h"
|
#import "DataManager.h"
|
||||||
@@ -42,18 +42,42 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
|||||||
|
|
||||||
NSLock *_controllerStreamLock;
|
NSLock *_controllerStreamLock;
|
||||||
NSMutableDictionary *_voidControllers;
|
NSMutableDictionary *_voidControllers;
|
||||||
id<ControllerSupportDelegate> _delegate;
|
|
||||||
StreamConfiguration* _streamConfig;
|
StreamConfiguration* _streamConfig;
|
||||||
|
__weak id<ControllerSupportDelegate> _delegate;
|
||||||
|
|
||||||
float accumulatedDeltaX;
|
float accumulatedDeltaX;
|
||||||
float accumulatedDeltaY;
|
float accumulatedDeltaY;
|
||||||
float accumulatedScrollX;
|
float accumulatedScrollX;
|
||||||
float accumulatedScrollY;
|
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;
|
OnScreenControls *_osc;
|
||||||
VoidController *_oscController;
|
VoidController *_oscController;
|
||||||
NSMutableSet* _activeGCControllers;
|
NSMutableSet* _activeGCControllers;
|
||||||
|
TemporarySettings* tempSettings;
|
||||||
|
OSCProfile* oscProfile;
|
||||||
|
OSCProfilesManager* oscProfileMan;
|
||||||
|
|
||||||
#define EMULATING_SELECT 0x1
|
#define EMULATING_SELECT 0x1
|
||||||
#define EMULATING_SPECIAL 0x2
|
#define EMULATING_SPECIAL 0x2
|
||||||
|
|
||||||
@@ -64,6 +88,17 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
|||||||
int _gyroMode;
|
int _gyroMode;
|
||||||
CGFloat _gyroSensitivity;
|
CGFloat _gyroSensitivity;
|
||||||
bool _captureMouse;
|
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)
|
// 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
|
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor
|
||||||
{
|
{
|
||||||
VoidController* voidController = [_voidControllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
|
VoidController* voidController = [_voidControllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
|
||||||
|
|
||||||
|
HapticEnginePreference preference = tempSettings.hapticEngine.intValue;
|
||||||
|
|
||||||
if (voidController == nil && controllerNumber == 0 && _oscEnabled) {
|
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) {
|
if (voidController == nil) {
|
||||||
// No connected controller for this player
|
// No connected controller for this player
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
[voidController.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
// physical controller connected:
|
||||||
[voidController.highFreqMotor setMotorAmplitude:highFreqMotor];
|
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
|
-(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];
|
[controller.rightTriggerMotor setMotorAmplitude:rightTrigger];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (bool)isLandscapeNow {
|
||||||
|
return CGRectGetWidth([[UIScreen mainScreen]bounds]) > CGRectGetHeight([[UIScreen mainScreen]bounds]);
|
||||||
|
}
|
||||||
|
|
||||||
-(void)updateTimerStateForController:(VoidController* )voidController{
|
-(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){
|
if(_gyroMode == GyroModeOff){
|
||||||
[self stopTimerForController:voidController];
|
[self stopTimerForController:voidController];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(voidController.gamepad.motion.hasAttitudeAndRotationRate) [voidController.motionTypes addObject:@(LI_MOTION_TYPE_ACCEL)];
|
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){
|
for(NSNumber* motionTypeObj in voidController.motionTypes){
|
||||||
uint8_t motionType = motionTypeObj.intValue;
|
uint8_t motionType = motionTypeObj.intValue;
|
||||||
@@ -147,20 +211,29 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
|||||||
voidController.lastDeviceAccelSample = deviceAccelSample;
|
voidController.lastDeviceAccelSample = deviceAccelSample;
|
||||||
|
|
||||||
// Convert g to m/s^2
|
// 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,
|
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||||
LI_MOTION_TYPE_ACCEL,
|
LI_MOTION_TYPE_ACCEL,
|
||||||
deviceAccelSample.y * -9.80665f * self->_gyroSensitivity,
|
deviceAccelSample.y * -9.80665f * self->_gyroSensitivity,
|
||||||
deviceAccelSample.z * -9.80665f * self->_gyroSensitivity,
|
deviceAccelSample.z * -9.80665f * self->_gyroSensitivity,
|
||||||
deviceAccelSample.x * -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;
|
break;
|
||||||
@@ -186,21 +259,68 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
|||||||
voidController.lastDeviceGyroSample = deviceGyroSample;
|
voidController.lastDeviceGyroSample = deviceGyroSample;
|
||||||
|
|
||||||
// Convert rad/s to deg/s
|
// 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
|
if(UIApplication.sharedApplication.windows.firstObject.windowScene.interfaceOrientation == 4){//check for landscape left or landscape right
|
||||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||||
LI_MOTION_TYPE_GYRO,
|
LI_MOTION_TYPE_GYRO,
|
||||||
deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity,
|
self->_gyroEnabledFlag ? deviceGyroSample.y * 57.2957795f * self->_gyroSensitivity : 0,
|
||||||
deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity,
|
self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0,
|
||||||
deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity);
|
self->_gyroEnabledFlag ? deviceGyroSample.x * 57.2957795f * self->_gyroSensitivity : 0);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||||
LI_MOTION_TYPE_GYRO,
|
LI_MOTION_TYPE_GYRO,
|
||||||
deviceGyroSample.y * -57.2957795f * self->_gyroSensitivity,
|
self->_gyroEnabledFlag ? deviceGyroSample.y * -57.2957795f * self->_gyroSensitivity : 0,
|
||||||
deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity,
|
self->_gyroEnabledFlag ? deviceGyroSample.z * 57.2957795f * self->_gyroSensitivity : 0,
|
||||||
deviceGyroSample.x * -57.2957795f * self->_gyroSensitivity);
|
self->_gyroEnabledFlag ? deviceGyroSample.x * -57.2957795f * self->_gyroSensitivity : 0);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}];
|
}];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -209,72 +329,75 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
|||||||
#endif
|
#endif
|
||||||
else{
|
else{
|
||||||
NSLog(@"controller obj timer update: controller timer ");
|
NSLog(@"controller obj timer update: controller timer ");
|
||||||
switch (motionType) {
|
|
||||||
case LI_MOTION_TYPE_ACCEL:
|
if (@available(iOS 14.0, *)) {
|
||||||
[voidController.accelTimer invalidate];
|
switch (motionType) {
|
||||||
voidController.accelTimer = nil;
|
case LI_MOTION_TYPE_ACCEL:
|
||||||
|
[voidController.accelTimer invalidate];
|
||||||
if (voidController.reportRateHz && voidController.gamepad.motion.hasGravityAndUserAcceleration) {
|
voidController.accelTimer = nil;
|
||||||
// Reset the last motion sample
|
|
||||||
GCAcceleration emptyAccelSample = {};
|
if (voidController.reportRateHz && voidController.gamepad.motion.hasGravityAndUserAcceleration) {
|
||||||
voidController.lastAccelSample = emptyAccelSample;
|
// Reset the last motion sample
|
||||||
NSLog(@"setup controller gyro accelTimer");
|
GCAcceleration emptyAccelSample = {};
|
||||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
voidController.lastAccelSample = emptyAccelSample;
|
||||||
voidController.hasAccelerometer = YES;
|
NSLog(@"setup controller gyro accelTimer");
|
||||||
voidController.accelTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) {
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||||
// Don't send duplicate samples
|
voidController.hasAccelerometer = YES;
|
||||||
GCAcceleration lastAccelSample = voidController.lastAccelSample;
|
voidController.accelTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) {
|
||||||
GCAcceleration accelSample = voidController.gamepad.motion.acceleration;
|
// Don't send duplicate samples
|
||||||
|
GCAcceleration lastAccelSample = voidController.lastAccelSample;
|
||||||
if (memcmp(&accelSample, &lastAccelSample, sizeof(accelSample)) == 0) {
|
GCAcceleration accelSample = voidController.gamepad.motion.acceleration;
|
||||||
return;
|
|
||||||
}
|
if (memcmp(&accelSample, &lastAccelSample, sizeof(accelSample)) == 0) {
|
||||||
voidController.lastAccelSample = accelSample;
|
return;
|
||||||
|
}
|
||||||
// Convert g to m/s^2
|
voidController.lastAccelSample = accelSample;
|
||||||
//NSLog(@"sending controller gyro data, accelSample data 00: %f, playerIndex: %ld, obj: %@",accelSample.x, (long)voidController.gamepad.playerIndex, voidController);
|
|
||||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
// Convert g to m/s^2
|
||||||
LI_MOTION_TYPE_ACCEL,
|
//NSLog(@"sending controller gyro data, accelSample data 00: %f, playerIndex: %ld, obj: %@",accelSample.x, (long)voidController.gamepad.playerIndex, voidController);
|
||||||
accelSample.x * -9.80665f * self->_gyroSensitivity,
|
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||||
accelSample.y * -9.80665f * self->_gyroSensitivity,
|
LI_MOTION_TYPE_ACCEL,
|
||||||
accelSample.z * -9.80665f * self->_gyroSensitivity);
|
accelSample.x * -9.80665f * self->_gyroSensitivity,
|
||||||
}];
|
accelSample.y * -9.80665f * self->_gyroSensitivity,
|
||||||
});
|
accelSample.z * -9.80665f * self->_gyroSensitivity);
|
||||||
}
|
}];
|
||||||
break;
|
});
|
||||||
|
}
|
||||||
case LI_MOTION_TYPE_GYRO:
|
break;
|
||||||
[voidController.gyroTimer invalidate];
|
|
||||||
voidController.gyroTimer = nil;
|
case LI_MOTION_TYPE_GYRO:
|
||||||
|
[voidController.gyroTimer invalidate];
|
||||||
if (voidController.reportRateHz && voidController.gamepad.motion.hasRotationRate) {
|
voidController.gyroTimer = nil;
|
||||||
// Reset the last motion sample
|
|
||||||
GCRotationRate emptyGyroSample = {};
|
if (voidController.reportRateHz && voidController.gamepad.motion.hasRotationRate) {
|
||||||
voidController.lastGyroSample = emptyGyroSample;
|
// Reset the last motion sample
|
||||||
//dispatch_sync(dispatch_get_main_queue(), ^{
|
GCRotationRate emptyGyroSample = {};
|
||||||
{dispatch_async(dispatch_get_main_queue(), ^{
|
voidController.lastGyroSample = emptyGyroSample;
|
||||||
voidController.hasGyroscope = YES;
|
//dispatch_sync(dispatch_get_main_queue(), ^{
|
||||||
voidController.gyroTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) {
|
{dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
// Don't send duplicate samples
|
voidController.hasGyroscope = YES;
|
||||||
GCRotationRate lastGyroSample = voidController.lastGyroSample;
|
voidController.gyroTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / voidController.reportRateHz repeats:YES block:^(NSTimer *timer) {
|
||||||
GCRotationRate gyroSample = voidController.gamepad.motion.rotationRate;
|
// Don't send duplicate samples
|
||||||
if (memcmp(&gyroSample, &lastGyroSample, sizeof(gyroSample)) == 0) {
|
GCRotationRate lastGyroSample = voidController.lastGyroSample;
|
||||||
return;
|
GCRotationRate gyroSample = voidController.gamepad.motion.rotationRate;
|
||||||
}
|
if (memcmp(&gyroSample, &lastGyroSample, sizeof(gyroSample)) == 0) {
|
||||||
voidController.lastGyroSample = gyroSample;
|
return;
|
||||||
|
}
|
||||||
// Convert rad/s to deg/s
|
voidController.lastGyroSample = gyroSample;
|
||||||
// NSLog(@"sending controller gyro data, gyroSample data 00: %f, playerIndex: %ld, obj: %@",gyroSample.x, (long)voidController.gamepad.playerIndex, voidController);
|
|
||||||
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
// Convert rad/s to deg/s
|
||||||
LI_MOTION_TYPE_GYRO,
|
// NSLog(@"sending controller gyro data, gyroSample data 00: %f, playerIndex: %ld, obj: %@",gyroSample.x, (long)voidController.gamepad.playerIndex, voidController);
|
||||||
gyroSample.x * 57.2957795f * self->_gyroSensitivity,
|
LiSendControllerMotionEvent((uint8_t)voidController.controllerNumber,
|
||||||
gyroSample.z * 57.2957795f * self->_gyroSensitivity,
|
LI_MOTION_TYPE_GYRO,
|
||||||
gyroSample.y * -57.2957795f * self->_gyroSensitivity);
|
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;
|
});}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,7 +406,7 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
|||||||
|
|
||||||
// Set the motion sensor state if they require manual activation
|
// Set the motion sensor state if they require manual activation
|
||||||
[self updateSensorSateForController:voidController];
|
[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 {
|
- (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);
|
NSLog(@"gyroMode: %ld", (long)_gyroMode);
|
||||||
|
|
||||||
VoidController* voidController = [_voidControllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
|
VoidController* voidController = [_voidControllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
|
||||||
@@ -322,7 +445,7 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
|||||||
voidController.controllerNumber = controllerNumber;
|
voidController.controllerNumber = controllerNumber;
|
||||||
|
|
||||||
if(voidController == _oscController) [self updateTimerStateForController:voidController];
|
if(voidController == _oscController) [self updateTimerStateForController:voidController];
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
-(void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b {
|
-(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
|
-(void) registerControllerCallbacks:(GCController*) controller
|
||||||
{
|
{
|
||||||
if (controller != NULL) {
|
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]];
|
VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
|
||||||
short leftStickX, leftStickY;
|
short leftStickX, leftStickY;
|
||||||
short rightStickX, rightStickY;
|
short rightStickX, rightStickY;
|
||||||
unsigned char leftTrigger, rightTrigger;
|
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) {
|
if (self->_swapABXYButtons) {
|
||||||
UPDATE_BUTTON_FLAG(voidController, B_FLAG, gamepad.buttonA.pressed);
|
UPDATE_BUTTON_FLAG(voidController, B_FLAG, gamepad.buttonA.pressed);
|
||||||
UPDATE_BUTTON_FLAG(voidController, A_FLAG, gamepad.buttonB.pressed);
|
UPDATE_BUTTON_FLAG(voidController, A_FLAG, gamepad.buttonB.pressed);
|
||||||
@@ -1011,12 +1369,15 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
|||||||
index:1];
|
index:1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leftStickX = self->controllerToMouseStick != LeftStickToMouse ? gamepad.leftThumbstick.xAxis.value * 0x7FFE : 0;
|
||||||
|
leftStickY = self->controllerToMouseStick != LeftStickToMouse ? gamepad.leftThumbstick.yAxis.value * 0x7FFE : 0;;
|
||||||
|
|
||||||
leftStickX = gamepad.leftThumbstick.xAxis.value * 0x7FFE;
|
rightStickX = self->controllerToMouseStick != RightStickToMouse ? gamepad.rightThumbstick.xAxis.value * 0x7FFE : 0;
|
||||||
leftStickY = gamepad.leftThumbstick.yAxis.value * 0x7FFE;
|
rightStickY = self->controllerToMouseStick != RightStickToMouse ? gamepad.rightThumbstick.yAxis.value * 0x7FFE : 0;
|
||||||
|
|
||||||
rightStickX = gamepad.rightThumbstick.xAxis.value * 0x7FFE;
|
self->stickToMouseInputX = self->controllerToMouseStick == LeftStickToMouse ? gamepad.leftThumbstick.xAxis.value : gamepad.rightThumbstick.xAxis.value;
|
||||||
rightStickY = gamepad.rightThumbstick.yAxis.value * 0x7FFE;
|
self->stickToMouseInputY = self->controllerToMouseStick == LeftStickToMouse ? gamepad.leftThumbstick.yAxis.value : gamepad.rightThumbstick.yAxis.value;
|
||||||
|
|
||||||
leftTrigger = gamepad.leftTrigger.value * 0xFF;
|
leftTrigger = gamepad.leftTrigger.value * 0xFF;
|
||||||
rightTrigger = gamepad.rightTrigger.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 updateTriggers:voidController left:leftTrigger right:rightTrigger];
|
||||||
[self updateFinished:voidController];
|
[self updateFinished:voidController];
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log(LOG_W, @"Tried to register controller callbacks on NULL controller");
|
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;
|
_oscController.playerIndex = 0;
|
||||||
|
|
||||||
|
oscProfile = [oscProfileMan getSelectedProfile];
|
||||||
|
motionHandler = [MotionHandler sharedWithProfile:oscProfile];
|
||||||
DataManager* dataMan = [[DataManager alloc] init];
|
DataManager* dataMan = [[DataManager alloc] init];
|
||||||
TemporarySettings* currentSettings = [dataMan getSettings];
|
tempSettings = [dataMan getSettings];
|
||||||
_oscEnabled = _oscEnabled || (OnScreenControlsLevel)[currentSettings.onscreenControls integerValue] != OnScreenControlsLevelOff || streamConfig.gyroMode != GyroModeOff;
|
|
||||||
_gyroSensitivity = currentSettings.gyroSensitivity.floatValue;
|
_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{
|
- (void)resetGyroInputForController:(VoidController* )voidController{
|
||||||
@@ -1409,7 +1816,7 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
|||||||
self = [super init];
|
self = [super init];
|
||||||
|
|
||||||
NSLog(@"controller support init");
|
NSLog(@"controller support init");
|
||||||
|
|
||||||
_delegate = delegate;
|
_delegate = delegate;
|
||||||
_controllerStreamLock = [[NSLock alloc] init];
|
_controllerStreamLock = [[NSLock alloc] init];
|
||||||
_voidControllers = [[NSMutableDictionary alloc] init];
|
_voidControllers = [[NSMutableDictionary alloc] init];
|
||||||
@@ -1553,8 +1960,14 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
|||||||
|
|
||||||
|
|
||||||
_oscController = [[VoidController alloc] init];
|
_oscController = [[VoidController alloc] init];
|
||||||
|
[self initializeControllerHaptics:_oscController];
|
||||||
_gyroMode = AlwaysDevice;
|
_gyroMode = AlwaysDevice;
|
||||||
|
|
||||||
|
_controllerMouseEnabledFlag = false;
|
||||||
|
|
||||||
|
_gyroEnabledFlag = false;
|
||||||
|
oscProfileMan = [OSCProfilesManager sharedManager:CGRectZero];
|
||||||
|
|
||||||
[self updateCommonConfig:streamConfig];
|
[self updateCommonConfig:streamConfig];
|
||||||
|
|
||||||
for(VoidController* voidController in _voidControllers.allValues){
|
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;
|
_shallDisableGyroHotSwitch = streamConfig.gyroMode == GyroModeOff && _voidControllers.allValues.count == 0;
|
||||||
NSLog(@"shallDisableGyroHotSwitch %d", _shallDisableGyroHotSwitch);
|
NSLog(@"shallDisableGyroHotSwitch %d", _shallDisableGyroHotSwitch);
|
||||||
|
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1693,6 +2107,8 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
|
|||||||
[self unregisterMouseCallbacks:mouse];
|
[self unregisterMouseCallbacks:mouse];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[self stopDisplayLink];
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@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 UIView* _view;
|
||||||
@property NSMutableArray *layoutChanges;
|
@property NSMutableArray *layoutChanges;
|
||||||
@property CALayer *layerBeingDragged;
|
@property CALayer *layerBeingDragged;
|
||||||
@property UIViewController *layoutToolVC;
|
@property (nonatomic, weak) UIViewController *layoutToolVC;
|
||||||
|
|
||||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport
|
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport
|
||||||
streamConfig:(StreamConfiguration*)streamConfig oscLevel:(int)oscLevel;
|
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
|
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
|
[self drawGuidelines]; // add the blue guidelines that appear when button is being tapped and dragged
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
@@ -55,35 +55,45 @@
|
|||||||
/**
|
/**
|
||||||
* 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
|
* 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 {
|
- (void) drawButtons:(OSCProfile* )profile {
|
||||||
[super setDPadCenter]; // Set custom position for D-Pad here
|
// [super regenLayerPool];
|
||||||
[super setAnalogStickPositions]; // Set custom position for analog sticks here
|
[super setDPadCenter:profile]; // Set custom position for D-Pad here
|
||||||
|
[super setAnalogStickPositions:profile]; // Set custom position for analog sticks here
|
||||||
[super drawBumpers];
|
[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
|
// create dPad background layer
|
||||||
self._dPadBackground = [CALayer layer];
|
if([self.OSCButtonLayerPool containsObject:self._upButton]){
|
||||||
self._dPadBackground.name = @"dPad";
|
[self.OSCButtonLayerPool addObject:self._dPadBackground];
|
||||||
self._dPadBackground.frame = CGRectMake(self.D_PAD_CENTER_X,
|
self._dPadBackground.name = @"dPad";
|
||||||
self.D_PAD_CENTER_Y,
|
self._dPadBackground.frame = CGRectMake(self.D_PAD_CENTER_X,
|
||||||
self._leftButton.frame.size.width * 2 + BUTTON_DIST,
|
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._leftButton.frame.size.width * 2 + BUTTON_DIST);
|
||||||
[self.OSCButtonLayers addObject:self._dPadBackground];
|
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
|
||||||
[_view.layer addSublayer:self._dPadBackground];
|
[self.OSCButtonLayerPool addObject:self._dPadBackground];
|
||||||
|
[_view.layer addSublayer:self._dPadBackground];
|
||||||
// add dPad buttons to parent layer
|
|
||||||
[self._dPadBackground addSublayer:self._downButton];
|
// add dPad buttons to parent layer
|
||||||
[self._dPadBackground addSublayer:self._rightButton];
|
[self._dPadBackground addSublayer:self._downButton];
|
||||||
[self._dPadBackground addSublayer:self._upButton];
|
[self._dPadBackground addSublayer:self._rightButton];
|
||||||
[self._dPadBackground addSublayer:self._leftButton];
|
[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];
|
[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
|
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 ((layerBeingDragged != button) && !button.isHidden) {
|
||||||
if ((horizontalGuideline.center.y < button.position.y + 1) &&
|
if ((horizontalGuideline.center.y < button.position.y + 1) &&
|
||||||
(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
|
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 ((layerBeingDragged != button) && !button.isHidden) {
|
||||||
if ((verticalGuideline.center.x < button.position.x + 1) &&
|
if ((verticalGuideline.center.x < button.position.x + 1) &&
|
||||||
(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 */
|
/* have guidelines follow wherever the user is touching on the screen */
|
||||||
horizontalGuideline.center = layerBeingDragged.position;
|
horizontalGuideline.center = layerBeingDragged.position;
|
||||||
verticalGuideline.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.
|
-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
|
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 ((layerBeingDragged != button) && !button.isHidden) {
|
||||||
if ((horizontalGuideline.center.y < button.position.y + 1) &&
|
if ((horizontalGuideline.center.y < button.position.y + 1) &&
|
||||||
(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
|
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 ((layerBeingDragged != button) && !button.isHidden) {
|
||||||
if ((verticalGuideline.center.x < button.position.x + 1) &&
|
if ((verticalGuideline.center.x < button.position.x + 1) &&
|
||||||
(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*/
|
/* returns reference to button layer object given the button's name*/
|
||||||
- (CALayer*)controllerLayerFromName: (NSString*)name {
|
- (CALayer*)controllerLayerFromName: (NSString*)name {
|
||||||
for (CALayer *buttonLayer in self.OSCButtonLayers) {
|
for (CALayer *buttonLayer in self.OSCButtonLayerPool) {
|
||||||
|
|
||||||
if ([buttonLayer.name isEqualToString:name]) {
|
if ([buttonLayer.name isEqualToString:name]) {
|
||||||
return buttonLayer;
|
return buttonLayer;
|
||||||
@@ -297,7 +307,7 @@
|
|||||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"LegacyOscCALayerSelectedNotification" object:layerBeingDragged]; // inform that a CALayer of onScreen controller has being selected
|
[[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 */
|
/* 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
|
// add hidden attr here
|
||||||
onScreenButtonState.isHidden = layerBeingDragged.isHidden;
|
onScreenButtonState.isHidden = layerBeingDragged.isHidden;
|
||||||
|
|
||||||
@@ -307,7 +317,7 @@
|
|||||||
/* make guide lines visible and position them over the button the user is touching */
|
/* make guide lines visible and position them over the button the user is touching */
|
||||||
horizontalGuideline.hidden = NO;
|
horizontalGuideline.hidden = NO;
|
||||||
verticalGuideline.hidden = NO;
|
verticalGuideline.hidden = NO;
|
||||||
[self updateGuidelinesForLegacyOscButton];
|
[self updateGuidelinesForLegacyOnScreenControls];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +327,7 @@
|
|||||||
[CATransaction begin];
|
[CATransaction begin];
|
||||||
[CATransaction setDisableActions:YES];
|
[CATransaction setDisableActions:YES];
|
||||||
layerBeingDragged.position = touchLocation; // move object to touch location
|
layerBeingDragged.position = touchLocation; // move object to touch location
|
||||||
[self updateGuidelinesForLegacyOscButton];
|
[self updateGuidelinesForLegacyOnScreenControls];
|
||||||
[CATransaction commit];
|
[CATransaction commit];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,17 +14,63 @@
|
|||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
#define DEFAULT_TEMPLATE_NAME1 @"控件模板 / Widget templates"
|
#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.
|
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
|
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 NSString *name;
|
||||||
@property NSMutableArray <OnScreenButtonState *> *buttonStates;
|
@property NSMutableArray <NSData *> *buttonStatesEncoded;
|
||||||
|
@property (nonatomic, assign) int16_t unfoldedExclusiveFolderSequence;
|
||||||
|
@property NSSet<NSNumber *> *postExclusiveUnfoldedSequences;
|
||||||
@property BOOL isSelected;
|
@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;
|
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected;
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,14 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "OSCProfile.h"
|
#import "OSCProfile.h"
|
||||||
|
#import "VoidLink-Swift.h"
|
||||||
|
|
||||||
@implementation OSCProfile
|
@implementation OSCProfile
|
||||||
|
|
||||||
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected {
|
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected {
|
||||||
if ((self = [self init])) {
|
if ((self = [self init])) {
|
||||||
self.name = name;
|
self.name = name;
|
||||||
self.buttonStates = buttonStates;
|
self.buttonStatesEncoded = buttonStates;
|
||||||
self.isSelected = isSelected;
|
self.isSelected = isSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,18 +30,177 @@
|
|||||||
|
|
||||||
- (void) encodeWithCoder:(NSCoder*)encoder {
|
- (void) encodeWithCoder:(NSCoder*)encoder {
|
||||||
[encoder encodeObject:self.name forKey:@"name"];
|
[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 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 {
|
- (id) initWithCoder:(NSCoder*)decoder {
|
||||||
if (self = [super init]) {
|
if (self = [super init]) {
|
||||||
self.name = [decoder decodeObjectForKey:@"name"];
|
self.name =
|
||||||
self.buttonStates = [decoder decodeObjectForKey:@"buttonStates"];
|
[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.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;
|
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
|
@end
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import "OSCProfile.h"
|
#import "OSCProfile.h"
|
||||||
|
#import "DataManager.h"
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
*/
|
*/
|
||||||
@interface OSCProfilesManager : NSObject
|
@interface OSCProfilesManager : NSObject
|
||||||
@property NSMutableArray <OSCProfile *> *currentProfiles;
|
@property NSMutableArray <OSCProfile *> *currentProfiles;
|
||||||
|
@property (atomic, assign) WidgetSizeTransition widgetSizeTransition;
|
||||||
|
|
||||||
+ (OSCProfilesManager *)sharedManager:(CGRect)viewBounds;
|
+ (OSCProfilesManager *)sharedManager:(CGRect)viewBounds;
|
||||||
+ (void)setOnScreenWidgetViewsSet:(NSMutableSet* )set;
|
+ (void)setOnScreenWidgetViewsSet:(NSMutableSet* )set;
|
||||||
@@ -38,28 +39,34 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
*/
|
*/
|
||||||
- (OSCProfile *) getSelectedProfile;
|
- (OSCProfile *) getSelectedProfile;
|
||||||
|
|
||||||
|
- (uint32_t) getIndexOfLastProfile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the index of the 'selected' profile within the array it's in
|
* Returns the index of the 'selected' profile within the array it's in
|
||||||
*/
|
*/
|
||||||
- (NSInteger) getIndexOfSelectedProfile;
|
- (NSInteger) getIndexOfSelectedProfile;
|
||||||
|
|
||||||
- (NSMutableArray *) getEncodedProfiles;
|
- (NSMutableArray *) getEncodedProfiles;
|
||||||
|
// - (NSData* )getSelectedEncodedProfile;
|
||||||
- (void) importEncodedProfiles:(NSMutableArray* )profilesEncoded;
|
- (void) importEncodedProfiles:(NSMutableArray* )profilesEncoded;
|
||||||
- (OnScreenButtonState *)unarchiveButtonStateEncoded:(NSData *)data;
|
- (OnScreenButtonState *)unarchiveButtonStateEncoded:(NSData *)data;
|
||||||
|
- (void) updateDefaultTemplates;
|
||||||
- (void) importDefaultTemplates;
|
- (void) importDefaultTemplates;
|
||||||
|
|
||||||
#pragma mark - Setters
|
#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
|
* 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.
|
* Delete current selected profile.
|
||||||
*/
|
*/
|
||||||
@@ -71,6 +78,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
*/
|
*/
|
||||||
- (BOOL) profileNameAlreadyExist:(NSString*)name;
|
- (BOOL) profileNameAlreadyExist:(NSString*)name;
|
||||||
- (OSCProfile *) findProfileByName:(NSString*) name inProfileArray:(NSMutableArray*)profiles;
|
- (OSCProfile *) findProfileByName:(NSString*) name inProfileArray:(NSMutableArray*)profiles;
|
||||||
|
- (CGPoint)normalizeWidgetPosition:(CGPoint)position;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ static CGRect layoutViewBounds;
|
|||||||
+ (OSCProfilesManager *) sharedManager:(CGRect)viewBounds {
|
+ (OSCProfilesManager *) sharedManager:(CGRect)viewBounds {
|
||||||
static OSCProfilesManager *_sharedManager = nil;
|
static OSCProfilesManager *_sharedManager = nil;
|
||||||
static dispatch_once_t onceToken;
|
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);
|
// NSLog(@"bounds width: %f, height: %f", layoutViewBounds.size.width, layoutViewBounds.size.height);
|
||||||
dispatch_once(&onceToken, ^{
|
dispatch_once(&onceToken, ^{
|
||||||
_sharedManager = [[self alloc] init];
|
_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
|
* 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 *) getEncodedProfiles {
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
NSData *profilesArrayEncoded = [[NSUserDefaults standardUserDefaults] objectForKey: @"OSCProfiles"]; // Get the encoded array of encoded OSC profiles from persistent storage
|
||||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
|
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;
|
return profilesEncoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) importEncodedProfiles:(NSMutableArray* )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)
|
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];
|
profile = [self findProfileByName:DEFAULT_TEMPLATE_NAME1 inProfileArray:targetProfiles];
|
||||||
if(profile && targetProfiles.count > 1) [targetProfiles removeObject:profile];
|
if(profile && targetProfiles.count > 1) [targetProfiles removeObject:profile];
|
||||||
profile = [self findProfileByName:DEFAULT_TEMPLATE_NAME2 inProfileArray:targetProfiles];
|
profile = [self findProfileByName:DEFAULT_TEMPLATE_NAME2 inProfileArray:targetProfiles];
|
||||||
if(profile && targetProfiles.count > 1) [targetProfiles removeObject:profile];
|
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];
|
if(targetProfiles.count > 0) [targetProfiles removeObjectAtIndex:0];
|
||||||
NSMutableArray* localEncodedPofiles = [self encodedProfilesFromArray:targetProfiles];
|
NSMutableArray* localEncodedPofiles = [self encodedProfilesFromArray:targetProfiles];
|
||||||
[profilesEncoded addObjectsFromArray:localEncodedPofiles];
|
[profilesEncoded addObjectsFromArray:localEncodedPofiles];
|
||||||
@@ -164,7 +194,6 @@ static CGRect layoutViewBounds;
|
|||||||
return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void) importDefaultTemplates{
|
- (void) importDefaultTemplates{
|
||||||
NSString *filePath = [[NSBundle mainBundle] pathForResource: [self isIPhone] ? @"widgetTemplatesIPhone": @"widgetTemplates" ofType:@"bin"];
|
NSString *filePath = [[NSBundle mainBundle] pathForResource: [self isIPhone] ? @"widgetTemplatesIPhone": @"widgetTemplates" ofType:@"bin"];
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
@@ -177,6 +206,47 @@ static CGRect layoutViewBounds;
|
|||||||
NSSet *classes = [NSSet setWithObjects: [NSMutableData class], [NSMutableArray class], nil];
|
NSSet *classes = [NSSet setWithObjects: [NSMutableData class], [NSMutableArray class], nil];
|
||||||
profilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:fileData error:&error];
|
profilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:fileData error:&error];
|
||||||
[self importEncodedProfiles:profilesEncoded];
|
[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,10 +257,9 @@ static CGRect layoutViewBounds;
|
|||||||
#pragma mark - Getters
|
#pragma mark - Getters
|
||||||
|
|
||||||
- (NSMutableArray *) getAllProfiles {
|
- (NSMutableArray *) getAllProfiles {
|
||||||
|
|
||||||
NSData *profilesArrayEncoded = [[NSUserDefaults standardUserDefaults] objectForKey: @"OSCProfiles"]; // Get the encoded array of encoded OSC profiles from persistent storage
|
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];
|
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
|
NSMutableArray *profilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:profilesArrayEncoded error:nil]; // Decode the encoded array itself, NOT the objects contained in the array
|
||||||
|
|
||||||
/* Decode each of the encoded profiles, place them into an array, then return the array */
|
/* Decode each of the encoded profiles, place them into an array, then return the array */
|
||||||
@@ -206,15 +275,27 @@ static CGRect layoutViewBounds;
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (OSCProfile *) getSelectedProfile {
|
- (OSCProfile *) getSelectedProfile {
|
||||||
|
NSLog(@"getAllProfiles test %f", CACurrentMediaTime());
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
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) {
|
for (OSCProfile *profile in profiles) {
|
||||||
|
|
||||||
if (profile.isSelected) {
|
if (profile.isSelected) {
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil;
|
return profiles[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSInteger) getIndexOfSelectedProfile {
|
- (NSInteger) getIndexOfSelectedProfile {
|
||||||
@@ -228,25 +309,23 @@ 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
|
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
|
#pragma mark - Setters
|
||||||
|
|
||||||
- (void) setProfileToSelected:(NSString *)name {
|
- (void) setProfileToSelected:(uint32_t)tableIndex {
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
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 */
|
[profiles enumerateObjectsUsingBlock:^(OSCProfile* profile, NSUInteger idx, BOOL *stop) {
|
||||||
for (OSCProfile *profile in profiles) {
|
profile.isSelected = idx == tableIndex;
|
||||||
|
}];
|
||||||
if ([profile.name isEqualToString:name]) {
|
|
||||||
profile.isSelected = YES;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
profile.isSelected = NO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
|
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
|
||||||
|
|
||||||
/* Encode the array itself, NOT the objects inside the array, which have already been encoded by this point */
|
/* Encode the array itself, NOT the objects inside the array, which have already been encoded by this point */
|
||||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
||||||
@@ -254,55 +333,45 @@ static CGRect layoutViewBounds;
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (bool) updateSelectedProfile:(NSMutableArray *) oscButtonLayers {
|
- (bool) updateSelectedProfile:(NSMutableSet *) oscButtonLayers {
|
||||||
NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndWidgetsToButtonStates:oscButtonLayers];
|
NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndWidgetsToButtonStates:oscButtonLayers];
|
||||||
if([self getIndexOfSelectedProfile]==0) return false;
|
if([self getIndexOfSelectedProfile]==0) return false;
|
||||||
|
/*
|
||||||
OSCProfile *newProfile = [[OSCProfile alloc] initWithName:[self getSelectedProfile].name
|
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
|
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
|
||||||
|
*/
|
||||||
|
OSCProfile *selectedProfile = [self getSelectedProfile];
|
||||||
/* set all saved OSCProfiles 'isSelected' property to NO since the new profile you're adding will be set as the selected profile */
|
selectedProfile.buttonStatesEncoded = buttonStatesEncoded;
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
selectedProfile.unfoldedExclusiveFolderSequence = OnScreenWidgetView.unfoldedExclusiveFolderSequence;
|
||||||
for (OSCProfile *profile in profiles) {
|
selectedProfile.postExclusiveUnfoldedSequences = OnScreenWidgetView.postExclusiveUnfoldedSequences;
|
||||||
profile.isSelected = NO;
|
[self replaceSelectedProfileWith:selectedProfile overwriteDefault:NO];
|
||||||
}
|
|
||||||
[self replaceProfile:[self getSelectedProfile] withProfile:newProfile];
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void) saveProfileWithName:(NSString*)name andButtonLayers:(NSMutableArray *)oscButtonLayers {
|
- (void) duplicateSelectedProfileWithName:(NSString*)name {
|
||||||
NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndWidgetsToButtonStates:oscButtonLayers];
|
if ([self profileNameAlreadyExist:name]) return;
|
||||||
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];
|
|
||||||
}
|
|
||||||
else { // otherwise encode then add the new profile to the end of the OSCProfiles array
|
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 *profiles = [self getAllProfiles];
|
||||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles];
|
OSCProfile* newProfile = nil;
|
||||||
[profilesEncoded addObject:newProfileEncoded];
|
for(OSCProfile* profile in profiles){
|
||||||
|
if(profile.isSelected){
|
||||||
/* Encode the 'profilesEncoded' array itself, NOT the objects in the 'profilesEncoded' array, all of which are already encoded by this point */
|
profile.isSelected = false;
|
||||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
newProfile = [profile mutableCopy];
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
newProfile.isSelected = true;
|
||||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
newProfile.name = name;
|
||||||
|
}
|
||||||
// saving test:
|
}
|
||||||
/*
|
if(newProfile){
|
||||||
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
|
NSData *newProfileEncoded = [NSKeyedArchiver archivedDataWithRootObject:newProfile requiringSecureCoding:YES error:nil];
|
||||||
NSString *path = [documentsPath stringByAppendingPathComponent:@"newDefault.bin"];
|
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles];
|
||||||
|
[profilesEncoded addObject:newProfileEncoded];
|
||||||
[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.x = position.x / layoutViewBounds.size.width;
|
||||||
position.y = position.y / layoutViewBounds.size.height;
|
position.y = position.y / layoutViewBounds.size.height;
|
||||||
}
|
}
|
||||||
// asdfsda;
|
NSLog(@"layoutToolView bounds: %f, %f", layoutViewBounds.size.width, layoutViewBounds.size.height);
|
||||||
NSLog(@"sef.view 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);
|
NSLog(@"position: %f, %f, denormalized position: %f, %f", position.x, position.y, newPosition.x, newPosition.y);
|
||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
@@ -326,30 +394,32 @@ static CGRect layoutViewBounds;
|
|||||||
return position;
|
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 */
|
/* 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:
|
NSSet *validPositionButtonNames = [NSSet setWithObjects:
|
||||||
@"l2Button",
|
@"l2Button",
|
||||||
@"l1Button",
|
@"l1Button",
|
||||||
@"dPad",
|
@"dPad",
|
||||||
@"selectButton",
|
@"selectButton",
|
||||||
@"leftStickBackground",
|
@"leftStickBackground",
|
||||||
@"rightStickBackground",
|
@"rightStickBackground",
|
||||||
@"r2Button",
|
@"r2Button",
|
||||||
@"r1Button",
|
@"r1Button",
|
||||||
@"aButton",
|
@"aButton",
|
||||||
@"bButton",
|
@"bButton",
|
||||||
@"xButton",
|
@"xButton",
|
||||||
@"yButton",
|
@"yButton",
|
||||||
@"startButton",
|
@"startButton",
|
||||||
nil]; */
|
nil]; */
|
||||||
NSMutableArray *buttonStatesEncoded = [[NSMutableArray alloc] init];
|
NSMutableArray *buttonStatesEncoded = [[NSMutableArray alloc] init];
|
||||||
|
|
||||||
// save on-screen game controller buttons & sticks as buttonstate:
|
// save on-screen game controller buttons & sticks as buttonstate:
|
||||||
for (CALayer *oscButtonLayer in oscButtonLayers) {
|
for (CALayer *oscButtonLayer in oscButtonLayers) {
|
||||||
|
if(oscButtonLayer.isHidden) continue;
|
||||||
|
|
||||||
CGPoint normalizedPosition = [self normalizeWidgetPosition:oscButtonLayer.position];
|
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
|
// add hidden attr here
|
||||||
buttonState.isHidden = oscButtonLayer.isHidden;
|
buttonState.isHidden = oscButtonLayer.isHidden;
|
||||||
buttonState.oscLayerSizeFactor = [OnScreenControls getControllerLayerSizeFactor:oscButtonLayer];
|
buttonState.oscLayerSizeFactor = [OnScreenControls getControllerLayerSizeFactor:oscButtonLayer];
|
||||||
@@ -370,40 +440,71 @@ static CGRect layoutViewBounds;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// save on-screen widget views (keyboard & mouse command) as buttonstate:
|
// save on-screen widget views (keyboard & mouse command) as buttonstate:
|
||||||
|
_widgetSizeTransition = keepWidgetSize;
|
||||||
for(OnScreenWidgetView* widgetView in OnScreenWidgetViews){
|
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];
|
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:widgetView.cmdString buttonType:CustomOnScreenWidget andPosition:normalizedPosition];
|
||||||
buttonState.alias = widgetView.buttonLabel;
|
buttonState.alias = widgetView.widgetLabel;
|
||||||
buttonState.widthFactor = [self normalizeSizeWidthFactor:widgetView];
|
buttonState.sequence = widgetView.sequence;
|
||||||
NSLog(@"logging widthFactor %f", buttonState.widthFactor);
|
buttonState.sequenceSet = widgetView.sequenceSet;
|
||||||
buttonState.heightFactor = [self normalizeSizeHeightFactor:widgetView];
|
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.backgroundAlpha = widgetView.backgroundAlpha;
|
||||||
|
buttonState.labelAlpha = widgetView.labelAlpha;
|
||||||
|
buttonState.borderAlpha = widgetView.borderAlpha;
|
||||||
|
buttonState.highlightAlpha = widgetView.highlightAlpha;
|
||||||
buttonState.borderWidth = widgetView.borderWidth;
|
buttonState.borderWidth = widgetView.borderWidth;
|
||||||
|
buttonState.highlightSizeFactor = widgetView.highlightSizeFactor;
|
||||||
|
buttonState.autoTapInterval = widgetView.autoTapInterval;
|
||||||
buttonState.vibrationStyle = widgetView.vibrationStyle;
|
buttonState.vibrationStyle = widgetView.vibrationStyle;
|
||||||
buttonState.mouseButtonAction = widgetView.mouseButtonAction;
|
buttonState.mouseButtonAction = widgetView.mouseButtonAction;
|
||||||
buttonState.sensitivityFactorX = widgetView.sensitivityFactorX;
|
buttonState.sensitivityFactorX = widgetView.sensitivityFactorX;
|
||||||
buttonState.sensitivityFactorY = widgetView.sensitivityFactorY;
|
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.stickIndicatorOffset = widgetView.stickIndicatorOffset;
|
||||||
buttonState.widgetShape = widgetView.shape;
|
buttonState.widgetShape = widgetView.shape;
|
||||||
|
buttonState.walkModeThreshold = widgetView.dWheelWalkModeThreshold;
|
||||||
buttonState.minStickOffset = widgetView.minStickOffset;
|
buttonState.minStickOffset = widgetView.minStickOffset;
|
||||||
buttonState.slideMode = widgetView.slideMode;
|
buttonState.buttonMode = widgetView.buttonMode;
|
||||||
|
|
||||||
NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
|
NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
|
||||||
[buttonStatesEncoded addObject: buttonStateEncoded];
|
[buttonStatesEncoded addObject: buttonStateEncoded];
|
||||||
|
|
||||||
}
|
}
|
||||||
return buttonStatesEncoded;
|
return buttonStatesEncoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (CGFloat)normalizeSizeWidthFactor:(OnScreenWidgetView* )widget{
|
- (CGFloat)getReferenceLen{
|
||||||
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
|
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)normalizeSizeWidthFactorWith:(OnScreenWidgetView* )widget{
|
||||||
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
|
return widget.bounds.size.width/[self getReferenceLen] * 10000;
|
||||||
return widget.bounds.size.height/screenWidthInPoints * 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 *)unarchiveButtonStateEncoded:(NSData *)data {
|
||||||
OnScreenButtonState* buttonState;
|
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
|
fromData:data
|
||||||
error:nil];
|
error:nil];
|
||||||
return buttonState;
|
return buttonState;
|
||||||
|
|||||||
@@ -24,46 +24,76 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
|
|
||||||
@property NSString *name;
|
@property NSString *name;
|
||||||
@property NSString *alias;
|
@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 CGPoint position;
|
||||||
@property (nonatomic, assign) BOOL isHidden;
|
@property (nonatomic, assign) BOOL isHidden;
|
||||||
@property (nonatomic, assign) uint8_t slideMode;
|
@property (nonatomic, assign) uint8_t buttonMode;
|
||||||
@property (nonatomic, assign) uint8_t buttonType;
|
@property (nonatomic, assign) uint8_t widgetType;
|
||||||
|
@property (nonatomic, assign) uint8_t sizeReference;
|
||||||
@property (nonatomic, assign) CGFloat widthFactor; // for OnScreenWidgetView
|
@property (nonatomic, assign) CGFloat widthFactor; // for OnScreenWidgetView
|
||||||
@property (nonatomic, assign) CGFloat heightFactor; // 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 borderWidth; // for OnScreenWidgetView
|
||||||
|
@property (nonatomic, assign) CGFloat highlightSizeFactor; // for OnScreenWidgetView
|
||||||
@property (nonatomic, assign) CGFloat sensitivityFactorX; // for OnScreenWidgetView
|
@property (nonatomic, assign) CGFloat sensitivityFactorX; // for OnScreenWidgetView
|
||||||
@property (nonatomic, assign) CGFloat sensitivityFactorY; // 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 stickIndicatorOffset; // for OnScreenWidgetView
|
||||||
|
@property (nonatomic, assign) CGFloat walkModeThreshold; // for OnScreenWidgetView
|
||||||
@property (nonatomic, assign) CGFloat minStickOffset; // for OnScreenWidgetView
|
@property (nonatomic, assign) CGFloat minStickOffset; // for OnScreenWidgetView
|
||||||
@property NSString* widgetShape; // for OnScreenWidgetView
|
@property NSString* widgetShape; // for OnScreenWidgetView
|
||||||
|
|
||||||
@property (nonatomic, assign) CGFloat oscLayerSizeFactor; // for OnScreenController CALayer
|
@property (nonatomic, assign) CGFloat oscLayerSizeFactor; // for OnScreenController CALayer
|
||||||
@property (nonatomic, assign) CGFloat backgroundAlpha; // for OnScreenController CALayer
|
@property (nonatomic, assign) CGFloat backgroundAlpha;
|
||||||
@property (nonatomic, assign) uint8_t vibrationStyle; // for OnScreenController CALayer
|
@property (nonatomic, assign) CGFloat labelAlpha;
|
||||||
@property (nonatomic, assign) uint8_t mouseButtonAction; // for OnScreenController CALayer
|
@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;
|
// @property (nonatomic, assign) BOOL hasValidPosition;
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, OnScreenButtonType) {
|
typedef NS_ENUM(NSInteger, OnScreenWidgetType) {
|
||||||
LegacyOscButton,
|
LegacyOnScreenControls,
|
||||||
CustomOnScreenWidget
|
CustomOnScreenWidget
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, WidgetSizeReference) {
|
||||||
|
longSide,
|
||||||
|
shortSide
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, MouseButtonAction) {
|
typedef NS_ENUM(NSInteger, MouseButtonAction) {
|
||||||
hovering,
|
hovering,
|
||||||
leftButtonDown,
|
leftButtonDown,
|
||||||
middleButtonDown,
|
middleButtonDown,
|
||||||
rightButtonDown,
|
rightButtonDown,
|
||||||
|
noClick
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, ButtonSlideMode) {
|
typedef NS_ENUM(NSInteger, ButtonMode) {
|
||||||
toggle,
|
slideToToggle,
|
||||||
slideAndHold,
|
slideAndHold,
|
||||||
disabled
|
regular,
|
||||||
|
tapToToggle,
|
||||||
|
movable
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSInteger, RevealMode) {
|
||||||
|
coexist,
|
||||||
|
exclusive
|
||||||
|
};
|
||||||
|
|
||||||
- (id) initWithButtonName:(NSString*)name buttonType:(uint8_t)buttonType andPosition:(CGPoint)position;
|
- (id) initWithButtonName:(NSString*)name buttonType:(uint8_t)buttonType andPosition:(CGPoint)position;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "OnScreenButtonState.h"
|
#import "OnScreenButtonState.h"
|
||||||
|
#import "VoidLink-Swift.h"
|
||||||
|
|
||||||
@implementation OnScreenButtonState
|
@implementation OnScreenButtonState
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
self.name = name;
|
self.name = name;
|
||||||
//self.isHidden = isHidden;
|
//self.isHidden = isHidden;
|
||||||
self.position = position;
|
self.position = position;
|
||||||
self.buttonType = buttonType;
|
self.widgetType = buttonType;
|
||||||
}
|
}
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
@@ -31,48 +32,82 @@
|
|||||||
|
|
||||||
- (void) encodeWithCoder:(NSCoder*)encoder {
|
- (void) encodeWithCoder:(NSCoder*)encoder {
|
||||||
[encoder encodeObject:self.name forKey:@"name"];
|
[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 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.vibrationStyle forKey:@"vibrationStyle"];
|
||||||
[encoder encodeInt:self.mouseButtonAction forKey:@"mouseButtonAction"];
|
[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 encodeCGPoint:self.position forKey:@"position"];
|
||||||
[encoder encodeBool:self.isHidden forKey:@"isHidden"];
|
[encoder encodeBool:self.isHidden forKey:@"isHidden"];
|
||||||
[encoder encodeFloat:self.widthFactor forKey:@"widthFactor"];
|
[encoder encodeFloat:self.widthFactor forKey:@"widthFactor"];
|
||||||
[encoder encodeFloat:self.heightFactor forKey:@"heightFactor"];
|
[encoder encodeFloat:self.heightFactor forKey:@"heightFactor"];
|
||||||
|
[encoder encodeFloat:self.componentSizeFactor forKey:@"componentSizeFactor"];
|
||||||
[encoder encodeFloat:self.sensitivityFactorX forKey:@"sensitivityFactorX"];
|
[encoder encodeFloat:self.sensitivityFactorX forKey:@"sensitivityFactorX"];
|
||||||
[encoder encodeFloat:self.sensitivityFactorY forKey:@"sensitivityFactorY"];
|
[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.stickIndicatorOffset forKey:@"stickIndicatorOffset"];
|
||||||
[encoder encodeFloat:self.oscLayerSizeFactor forKey:@"oscLayerSizeFactor"];
|
[encoder encodeFloat:self.oscLayerSizeFactor forKey:@"oscLayerSizeFactor"];
|
||||||
[encoder encodeFloat:self.backgroundAlpha forKey:@"backgroundAlpha"];
|
[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.borderWidth forKey:@"borderWidth"];
|
||||||
|
[encoder encodeFloat:self.highlightSizeFactor forKey:@"highlightSizeFactor"];
|
||||||
[encoder encodeObject:self.widgetShape forKey:@"widgetShape"];
|
[encoder encodeObject:self.widgetShape forKey:@"widgetShape"];
|
||||||
|
[encoder encodeFloat:self.walkModeThreshold forKey:@"walkModeThreshold"];
|
||||||
[encoder encodeFloat:self.minStickOffset forKey:@"minStickOffset"];
|
[encoder encodeFloat:self.minStickOffset forKey:@"minStickOffset"];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id) initWithCoder:(NSCoder*)decoder {
|
- (id) initWithCoder:(NSCoder*)decoder {
|
||||||
if (self = [super init]) {
|
if (self = [super init]) {
|
||||||
self.name = [decoder decodeObjectForKey:@"name"];
|
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.alias = [decoder decodeObjectForKey:@"alias"];
|
||||||
self.buttonType = [decoder decodeIntForKey:@"buttonType"];
|
self.widgetType = [decoder decodeIntForKey:@"buttonType"];
|
||||||
self.vibrationStyle = [decoder decodeIntForKey:@"vibrationStyle"];
|
self.sizeReference = [decoder containsValueForKey:@"sizeReference"] ? [decoder decodeIntForKey:@"sizeReference"] : longSide;
|
||||||
|
self.vibrationStyle = [decoder containsValueForKey:@"vibrationStyle"] ? [decoder decodeIntForKey:@"vibrationStyle"] : UIImpactFeedbackStyleLight;
|
||||||
self.mouseButtonAction = [decoder decodeIntForKey:@"mouseButtonAction"];
|
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.position = [decoder decodeCGPointForKey:@"position"];
|
||||||
self.isHidden = [decoder decodeBoolForKey:@"isHidden"];
|
self.isHidden = [decoder decodeBoolForKey:@"isHidden"];
|
||||||
self.widthFactor = [decoder decodeFloatForKey:@"widthFactor"];
|
self.widthFactor = [decoder decodeFloatForKey:@"widthFactor"];
|
||||||
self.heightFactor = [decoder decodeFloatForKey:@"heightFactor"];
|
self.heightFactor = [decoder decodeFloatForKey:@"heightFactor"];
|
||||||
self.sensitivityFactorX = [decoder decodeFloatForKey:@"sensitivityFactorX"];
|
self.componentSizeFactor = [decoder containsValueForKey:@"componentSizeFactor"] ? [decoder decodeFloatForKey:@"componentSizeFactor"] : 1.0;
|
||||||
self.sensitivityFactorX = self.sensitivityFactorX == 0 ? 1.0 : self.sensitivityFactorX;
|
self.sensitivityFactorX = [decoder containsValueForKey:@"sensitivityFactorX"] ? [decoder decodeFloatForKey:@"sensitivityFactorX"] : 1.0;
|
||||||
self.sensitivityFactorY = [decoder decodeFloatForKey:@"sensitivityFactorY"];
|
self.sensitivityFactorY = [decoder containsValueForKey:@"sensitivityFactorY"] ? [decoder decodeFloatForKey:@"sensitivityFactorY"] : 1.0;
|
||||||
self.sensitivityFactorY = self.sensitivityFactorY == 0 ? 1.0 : self.sensitivityFactorY;
|
self.slideThreshold = [decoder containsValueForKey:@"slideThreshold"] ? [decoder decodeFloatForKey:@"slideThreshold"] : 6.0;
|
||||||
self.decelerationRate = [decoder decodeFloatForKey:@"decelerationRate"];
|
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.stickIndicatorOffset = [decoder decodeFloatForKey:@"stickIndicatorOffset"];
|
||||||
self.oscLayerSizeFactor = [decoder decodeFloatForKey:@"oscLayerSizeFactor"];
|
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.borderWidth = [decoder decodeFloatForKey:@"borderWidth"];
|
||||||
|
self.highlightSizeFactor = [decoder containsValueForKey:@"highlightSizeFactor"] ? [decoder decodeFloatForKey:@"highlightSizeFactor"] : 1.0;
|
||||||
self.widgetShape = [decoder decodeObjectForKey:@"widgetShape"];
|
self.widgetShape = [decoder decodeObjectForKey:@"widgetShape"];
|
||||||
|
self.walkModeThreshold = [decoder containsValueForKey:@"walkModeThreshold"] ? [decoder decodeFloatForKey:@"walkModeThreshold"] : 16383;
|
||||||
self.minStickOffset = [decoder decodeFloatForKey:@"minStickOffset"];
|
self.minStickOffset = [decoder decodeFloatForKey:@"minStickOffset"];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
CGFloat lowestTouchPointYCoord;
|
CGFloat lowestTouchPointYCoord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property (nonatomic, weak) UIView* touchCapturingView;
|
||||||
@property (nonatomic, assign) uint8_t numberOfTouchesRequired;
|
@property (nonatomic, assign) uint8_t numberOfTouchesRequired;
|
||||||
@property (nonatomic, assign) bool immediateTriggering; // if enabled, trigger the signal on touchesBegan stage.
|
@property (nonatomic, assign) bool immediateTriggering; // if enabled, trigger the signal on touchesBegan stage.
|
||||||
@property (nonatomic, assign) double tapDownTimeThreshold; // tap down threshold in seconds.
|
@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 {
|
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||||
// Check if the number of touches and taps meets the required criteria
|
// Check if the number of touches and taps meets the required criteria
|
||||||
if ([[event allTouches] count] == _numberOfTouchesRequired) {
|
NSSet* allTouches = [event allTouches];
|
||||||
_gestureCapturedTime = CACurrentMediaTime();
|
if (allTouches.count == _numberOfTouchesRequired) {
|
||||||
_gestureCaptured = true;
|
if([UITouchUtil touchesIn:_touchCapturingView from:event].count != allTouches.count){
|
||||||
for(UITouch *touch in [event allTouches]){
|
_gestureCaptured = false;
|
||||||
if(lowestTouchPointYCoord < [touch locationInView:self.view].y) lowestTouchPointYCoord = [touch locationInView:self.view].y;
|
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.
|
// this mechanism is deprecated.
|
||||||
/* if(_numberOfTouchesRequired == 2){
|
/* if(_numberOfTouchesRequired == 2){
|
||||||
NSArray *twoTouches = [[event allTouches] allObjects];
|
NSArray *twoTouches = [[event allTouches] allObjects];
|
||||||
@@ -55,13 +63,13 @@ static CGFloat screenWidthInPoints;
|
|||||||
_lowestTouchPointHeight = screenHeightInPoints - lowestTouchPointYCoord;
|
_lowestTouchPointHeight = screenHeightInPoints - lowestTouchPointYCoord;
|
||||||
// NSLog(@"gen _lowestTouchPointHeight %f markmark", _lowestTouchPointHeight);
|
// NSLog(@"gen _lowestTouchPointHeight %f markmark", _lowestTouchPointHeight);
|
||||||
if(_immediateTriggering){
|
if(_immediateTriggering){
|
||||||
lowestTouchPointYCoord = 0.0; //reset for next recoginition
|
// lowestTouchPointYCoord = 0.0; //reset for next recoginition
|
||||||
self.state = UIGestureRecognizerStateRecognized;
|
self.state = UIGestureRecognizerStateRecognized;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.state = UIGestureRecognizerStatePossible;
|
self.state = UIGestureRecognizerStatePossible;
|
||||||
}
|
}
|
||||||
if ([[event allTouches] count] > _numberOfTouchesRequired) {
|
if (allTouches.count > _numberOfTouchesRequired) {
|
||||||
_gestureCaptured = false;
|
_gestureCaptured = false;
|
||||||
self.state = UIGestureRecognizerStateFailed;
|
self.state = UIGestureRecognizerStateFailed;
|
||||||
}
|
}
|
||||||
@@ -76,40 +84,26 @@ static CGFloat screenWidthInPoints;
|
|||||||
_gestureCaptured = false;
|
_gestureCaptured = false;
|
||||||
self.state = UIGestureRecognizerStateFailed;
|
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
|
_gestureCaptured = false; //reset for next recognition
|
||||||
if((CACurrentMediaTime() - _gestureCapturedTime) < _tapDownTimeThreshold){
|
if((CACurrentMediaTime() - _gestureCapturedTime) < _tapDownTimeThreshold){
|
||||||
lowestTouchPointYCoord = 0.0; //reset for next recognition
|
// lowestTouchPointYCoord = 0.0; //reset for next recognition
|
||||||
self.state = UIGestureRecognizerStateRecognized;
|
|
||||||
|
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);
|
// NSLog(@"gen _lowestTouchPointHeight %f markmark touchesEnd", _lowestTouchPointHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (allTouchesCount == [touches count]) _isOnScreenControllerBeingPressed = false; // need to reset this flag anyway, when all fingers are lefting
|
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
|
@end
|
||||||
|
|||||||
@@ -5,8 +5,12 @@
|
|||||||
// Created by Cameron Gutman on 9/17/20.
|
// Created by Cameron Gutman on 9/17/20.
|
||||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
// 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 "HapticContext.h"
|
||||||
|
#import "DataManager.h"
|
||||||
|
|
||||||
@import CoreHaptics;
|
@import CoreHaptics;
|
||||||
@import GameController;
|
@import GameController;
|
||||||
@@ -83,19 +87,74 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
-(id) initWithGamepad:(GCController*)gamepad locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
-(id) initDeviceEngineContextWithGamepad:(GCController*)gamepad API_AVAILABLE(ios(13.0), tvos(13.0)) {
|
||||||
if (gamepad.haptics == nil) {
|
NSError *error = nil;
|
||||||
Log(LOG_W, @"Controller %d does not support haptics", gamepad.playerIndex);
|
_hapticEngine = [[CHHapticEngine alloc] initAndReturnError:&error];
|
||||||
|
if (error != nil) {
|
||||||
|
Log(LOG_W, @"Controller %d: iPhone Haptic engine failed to start: %@", gamepad.playerIndex, error);
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
_playerIndex = gamepad.playerIndex;
|
||||||
|
|
||||||
if (![[gamepad.haptics supportedLocalities] containsObject:locality]) {
|
[_hapticEngine startAndReturnError:&error];
|
||||||
Log(LOG_W, @"Controller %d does not support haptic locality: %@", gamepad.playerIndex, locality);
|
if (error != nil) {
|
||||||
|
Log(LOG_W, @"Controller %d: Haptic engine failed to start: %@", gamepad.playerIndex, error);
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__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);
|
||||||
|
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;
|
_playerIndex = gamepad.playerIndex;
|
||||||
_hapticEngine = [gamepad.haptics createEngineWithLocality:locality];
|
|
||||||
|
|
||||||
NSError* error;
|
NSError* error;
|
||||||
[_hapticEngine startAndReturnError:&error];
|
[_hapticEngine startAndReturnError:&error];
|
||||||
@@ -136,7 +195,9 @@
|
|||||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightHandle];
|
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightHandle];
|
||||||
}
|
}
|
||||||
else {
|
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];
|
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftHandle];
|
||||||
}
|
}
|
||||||
else {
|
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];
|
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftTrigger];
|
||||||
}
|
}
|
||||||
else {
|
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];
|
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightTrigger];
|
||||||
}
|
}
|
||||||
else {
|
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
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@interface NativeTouchHandler : UIResponder
|
@interface NativeTouchHandler : UIResponder
|
||||||
|
|
||||||
@property (nonatomic, assign) NSMutableSet<UITouch *>* touchesCapturedByOnScreenButtons;
|
@property (nonatomic, assign) NSMutableSet<UITouch *>* touchesCapturedByOnScreenButtons;
|
||||||
|
|
||||||
|
|
||||||
- (id)initWithView:(StreamView* )view andSettings:(TemporarySettings*)settings;
|
- (id)initWithView:(StreamView* )view andSettings:(TemporarySettings*)settings;
|
||||||
|
- (void)setAllowSingleTouchEnabled:(BOOL)enabled;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,11 @@
|
|||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
|
|
||||||
@implementation NativeTouchHandler {
|
@implementation NativeTouchHandler {
|
||||||
StreamView* streamView;
|
__weak StreamView* streamView;
|
||||||
TemporarySettings* currentSettings;
|
|
||||||
bool activateCoordSelector;
|
bool activateCoordSelector;
|
||||||
|
bool singleTouchDisabled;
|
||||||
CGFloat pointerVelocityDividerLocationByPoints;
|
CGFloat pointerVelocityDividerLocationByPoints;
|
||||||
uint16_t touchMoveEventIntervalUs;
|
|
||||||
|
|
||||||
bool asyncNativeTouch;
|
bool asyncNativeTouch;
|
||||||
unsigned int touchDownQos;
|
unsigned int touchDownQos;
|
||||||
@@ -35,29 +33,37 @@
|
|||||||
NSMutableSet<NSNumber *> *pointerIdPool; //pre-defined pool of pointerIds.
|
NSMutableSet<NSNumber *> *pointerIdPool; //pre-defined pool of pointerIds.
|
||||||
NSMutableSet<NSNumber *> *unassignedPointerIds;
|
NSMutableSet<NSNumber *> *unassignedPointerIds;
|
||||||
NSMutableSet *blacklistedTouches;
|
NSMutableSet *blacklistedTouches;
|
||||||
|
|
||||||
|
bool trackPointEnabled;
|
||||||
|
NSMutableArray<CAShapeLayer *> * trackPointPool; //pre-defined pool of pointerIds.
|
||||||
|
|
||||||
NSMutableDictionary *pointerObjDict;
|
NSMutableDictionary *pointerObjDict;
|
||||||
|
|
||||||
CGFloat slideGestureVerticalThreshold;
|
CGFloat slideGestureVerticalThreshold;
|
||||||
CGFloat screenWidthWithThreshold;
|
CGFloat screenWidthWithThreshold;
|
||||||
CGFloat EDGE_TOLERANCE;
|
CGFloat _edgeTolerance;
|
||||||
|
|
||||||
CGRect streamViewBounds;
|
CGRect streamViewBounds;
|
||||||
|
|
||||||
|
int64_t moveEventIntervalNSec;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings{
|
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings{
|
||||||
self = [super init];
|
self = [super init];
|
||||||
self->streamView = view;
|
self->streamView = view;
|
||||||
self->currentSettings = settings;
|
self->activateCoordSelector = settings.pointerVelocityModeDivider.floatValue != 1.0;
|
||||||
self->activateCoordSelector = currentSettings.pointerVelocityModeDivider.floatValue != 1.0;
|
self->moveEventIntervalNSec = (int64_t)(settings.touchMoveEventInterval.intValue * 1000);;
|
||||||
self->touchMoveEventIntervalUs = currentSettings.touchMoveEventInterval.intValue;
|
|
||||||
self->streamViewBounds = view.bounds;
|
self->streamViewBounds = view.bounds;
|
||||||
|
|
||||||
self->pointerIdDict = [NSMutableDictionary dictionary];
|
self->pointerIdDict = [NSMutableDictionary dictionary];
|
||||||
self->pointerIdPool = [NSMutableSet set];
|
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
|
for (uint8_t i = 0; i <= 10; i++) { //ipadOS supports upto 11 finger touches
|
||||||
[self->pointerIdPool addObject:@(i)];
|
[self->pointerIdPool addObject:@(i)];
|
||||||
|
[self->trackPointPool addObject:[GraphicUtils makeTouchTrackpointIn:streamView]];
|
||||||
}
|
}
|
||||||
|
|
||||||
self->activePointerIds = [NSMutableSet set];
|
self->activePointerIds = [NSMutableSet set];
|
||||||
self->blacklistedTouches = [NSMutableSet set];
|
self->blacklistedTouches = [NSMutableSet set];
|
||||||
|
|
||||||
@@ -85,9 +91,9 @@
|
|||||||
|
|
||||||
self->pointerObjDict = [NSMutableDictionary dictionary];
|
self->pointerObjDict = [NSMutableDictionary dictionary];
|
||||||
|
|
||||||
EDGE_TOLERANCE = 10.0;
|
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
|
||||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
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;
|
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
|
//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];
|
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];
|
[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(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([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;
|
CGPoint targetCoords;
|
||||||
//NSLog(@"selecting coords: %d", touch.phase == UITouchPhaseMoved);
|
//NSLog(@"selecting coords: %d", touch.phase == UITouchPhaseMoved);
|
||||||
// NSLog(@"excluded count: %d", (uint32_t)[excludedPointerIds count]);
|
// NSLog(@"excluded count: %d", (uint32_t)[excludedPointerIds count]);
|
||||||
@@ -210,78 +224,87 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (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){
|
for (UITouch* touch in touches){
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
// 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
|
[self handleTouchDown:touch]; //generate & populate pointerId
|
||||||
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
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 {
|
- (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){
|
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.touchesCapturedByOnScreenControls containsObject:touch]) continue;
|
||||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
|
||||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE withEvent:event];
|
||||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
[[self getPointerObjFromDict:touch] doesNeedResetCoords];
|
||||||
usleep(self->touchMoveEventIntervalUs);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
else {
|
|
||||||
for (UITouch* touch in touches){
|
if(self->trackPointEnabled){
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
[CATransaction begin];
|
||||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
[CATransaction setDisableActions:YES];
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
for (UITouch* touch in touches){
|
||||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||||
// usleep(self->touchMoveEventIntervalUs);
|
CAShapeLayer* trackPoint = self->trackPointPool[pointerId];
|
||||||
}
|
trackPoint.position = [touch locationInView:self->streamView];
|
||||||
|
}
|
||||||
|
[CATransaction commit];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (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){
|
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.touchesCapturedByOnScreenControls containsObject:touch]){
|
||||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
[OnScreenControls.touchesCapturedByOnScreenControls removeObject:touch];
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
continue;
|
||||||
[self removePointerId:touch]; //then remove pointerId
|
}
|
||||||
|
[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];
|
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
||||||
[self->blacklistedTouches removeObject:@((uintptr_t)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 {
|
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
[self touchesEnded:touches withEvent:event];
|
[self touchesEnded:touches withEvent:event];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void)populatePointerObjIntoDict:(UITouch*)touch{
|
- (void)populatePointerObjIntoDict:(UITouch*)touch{
|
||||||
NativeTouchPointer* pointer = [[NativeTouchPointer alloc] initWithTouch:touch];
|
NativeTouchPointer* pointer = [[NativeTouchPointer alloc] initWithTouch:touch];
|
||||||
pointer.useRelativeCoords = pointer.initialPoint.x > pointerVelocityDividerLocationByPoints;
|
pointer.useRelativeCoords = pointer.initialPoint.x > pointerVelocityDividerLocationByPoints;
|
||||||
@@ -310,4 +333,22 @@
|
|||||||
return pointer.useRelativeCoords ? pointer.latestRelativePoint : pointer.latestPoint;
|
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
|
@end
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
|
|
||||||
|
|
||||||
+ (void)initContextWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
+ (void)initContextWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
||||||
|
+ (void)cleanUpContext;
|
||||||
|
|
||||||
- (bool)doesNeedResetCoords;
|
- (bool)doesNeedResetCoords;
|
||||||
- (void)updatePointerCoords:(UITouch *)touch;
|
- (void)updatePointerCoords:(UITouch *)touch;
|
||||||
|
|||||||
@@ -63,4 +63,8 @@ static StreamView* streamView;
|
|||||||
NSLog(@"stream wdith %f, stream height %f", streamViewWidth, streamViewHeight);
|
NSLog(@"stream wdith %f, stream height %f", streamViewWidth, streamViewHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+ (void)cleanUpContext {
|
||||||
|
streamView = nil;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -17,12 +17,7 @@
|
|||||||
|
|
||||||
@class ControllerSupport;
|
@class ControllerSupport;
|
||||||
@class StreamConfiguration;
|
@class StreamConfiguration;
|
||||||
|
@class LayoutOnScreenControls;
|
||||||
// sending self as an instance to OnScreenWidgetView or other related classes
|
|
||||||
@protocol InstanceProviderDelegate <NSObject>
|
|
||||||
- (void)getOnScreenControlsInstance:(id)sender; // Renamed delegate method
|
|
||||||
@end
|
|
||||||
|
|
||||||
|
|
||||||
static const float D_PAD_DIST = 10;
|
static const float D_PAD_DIST = 10;
|
||||||
static const float BUTTON_DIST = 20;
|
static const float BUTTON_DIST = 20;
|
||||||
@@ -36,13 +31,13 @@ static const float BUTTON_DIST = 20;
|
|||||||
CGFloat _dPadSizeFactor;
|
CGFloat _dPadSizeFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (NSMutableDictionary *)layerVibrationStyleDic;
|
+ (NSMutableDictionary *_Nullable)layerVibrationStyleDic;
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
||||||
OnScreenControlsLevelOff,
|
OnScreenControlsLevelOff,
|
||||||
|
OnScreenControlsLevelCustom,
|
||||||
OnScreenControlsLevelSimple,
|
OnScreenControlsLevelSimple,
|
||||||
OnScreenControlsLevelFull,
|
OnScreenControlsLevelFull,
|
||||||
OnScreenControlsLevelCustom,
|
|
||||||
OnScreenControlsLevelAuto, // move it here instead of delete , to ensure integrity of the codes
|
OnScreenControlsLevelAuto, // move it here instead of delete , to ensure integrity of the codes
|
||||||
|
|
||||||
// Internal levels selected by ControllerSupport
|
// 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) CustomTapGestureRecognizer* mouseRightClickTapRecognizer; // this object will be passed to onscreencontrols class for areVirtualControllerTaps flag setting
|
||||||
@property (nonatomic, assign) bool isLayingOut;
|
@property (nonatomic, assign) bool isLayingOut;
|
||||||
//@property (nonatomic) NSMutableSet<UITouch* >* touchesCapturedByOnScreenButtons;
|
//@property (nonatomic) NSMutableSet<UITouch* >* touchesCapturedByOnScreenButtons;
|
||||||
@@ -105,12 +95,15 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
|||||||
|
|
||||||
@property OnScreenControlsLevel _level;
|
@property OnScreenControlsLevel _level;
|
||||||
|
|
||||||
@property NSMutableArray *OSCButtonLayers;
|
@property NSMutableSet *OSCButtonLayerPool;
|
||||||
@property NSMutableDictionary* layerVibrationStyleDic;
|
@property NSMutableDictionary* layerVibrationStyleDic;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+ (NSMutableSet* )touchAddrsCapturedByOnScreenControls;
|
+ (NSMutableSet* )touchesCapturedByOnScreenControls;
|
||||||
|
+ (OnScreenControls* )shared;
|
||||||
|
+ (void)setShared:(OnScreenControls*)instance;
|
||||||
|
|
||||||
- (void) sendRightStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
|
- (void) sendRightStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
|
||||||
- (void) clearRightStickTouchPadFlag;
|
- (void) clearRightStickTouchPadFlag;
|
||||||
- (void) sendLeftStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
|
- (void) sendLeftStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
|
||||||
@@ -125,18 +118,19 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
|||||||
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
|
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
|
||||||
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
|
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
|
||||||
- (void) setLevel:(OnScreenControlsLevel)level;
|
- (void) setLevel:(OnScreenControlsLevel)level;
|
||||||
- (void) show;
|
- (void) showLegacyWidgetsWith:(OSCProfile* )profile;
|
||||||
|
- (void) regenLayerPool;
|
||||||
- (void) setupComplexControls;
|
- (void) setupComplexControls;
|
||||||
- (void) drawButtons;
|
- (void) drawButtons:(OSCProfile* _Nullable)profile;
|
||||||
- (void) drawBumpers;
|
- (void) drawBumpers;
|
||||||
- (void) updateControls;
|
- (void) updateLegacyWidgetsWith:(OSCProfile* _Nullable )profile;
|
||||||
- (OnScreenControlsLevel) getLevel;
|
- (OnScreenControlsLevel) getLevel;
|
||||||
- (void) setDPadCenter;
|
- (void) setDPadCenter:(OSCProfile *_Nonnull)profile;;
|
||||||
- (void) setAnalogStickPositions;
|
- (void) setAnalogStickPositions:(OSCProfile *_Nonnull)profile;
|
||||||
- (void) positionAndResizeSingleControllerLayers;
|
- (void) positionAndResizeSingleControllerLayers:(OSCProfile *_Nonnull)profile;
|
||||||
- (void) resizeControllerLayerWith:(CALayer*)layer and:(CGFloat)sizeFactor;
|
- (void) resizeControllerLayerWith:(CALayer*_Nonnull)layer and:(CGFloat)sizeFactor;
|
||||||
- (void) adjustControllerLayerOpacityWith:(CALayer*)layer and:(CGFloat)alpha;
|
- (void) adjustControllerLayerOpacityWith:(CALayer*_Nonnull)layer and:(CGFloat)alpha;
|
||||||
+ (CGFloat) getControllerLayerSizeFactor:(CALayer*)layer;
|
+ (CGFloat) getControllerLayerSizeFactor:(CALayer*_Nonnull)layer;
|
||||||
- (CGFloat) getControllerLayerOpacity:(CALayer*)layer;
|
- (CGFloat) getControllerLayerOpacity:(CALayer*_Nonnull)layer;
|
||||||
|
|
||||||
@end
|
@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
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@interface PureNativeTouchHandler : UIResponder
|
@interface PureNativeTouchHandler : UIResponder
|
||||||
|
|
||||||
|
|
||||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,9 @@
|
|||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
@implementation PureNativeTouchHandler {
|
@implementation PureNativeTouchHandler {
|
||||||
StreamView* streamView;
|
__weak StreamView* streamView;
|
||||||
TemporarySettings* currentSettings;
|
|
||||||
bool activateCoordSelector;
|
bool activateCoordSelector;
|
||||||
CGFloat pointerVelocityDividerLocationByPoints;
|
CGFloat pointerVelocityDividerLocationByPoints;
|
||||||
uint16_t touchMoveEventIntervalUs;
|
|
||||||
|
|
||||||
bool asyncNativeTouch;
|
bool asyncNativeTouch;
|
||||||
unsigned int touchDownQos;
|
unsigned int touchDownQos;
|
||||||
@@ -34,29 +32,38 @@
|
|||||||
NSMutableSet<NSNumber *> *pointerIdPool; //pre-defined pool of pointerIds.
|
NSMutableSet<NSNumber *> *pointerIdPool; //pre-defined pool of pointerIds.
|
||||||
NSMutableSet<NSNumber *> *unassignedPointerIds;
|
NSMutableSet<NSNumber *> *unassignedPointerIds;
|
||||||
NSMutableSet *blacklistedTouches;
|
NSMutableSet *blacklistedTouches;
|
||||||
|
|
||||||
|
bool trackPointEnabled;
|
||||||
|
NSMutableArray<CAShapeLayer *> * trackPointPool;
|
||||||
|
|
||||||
NSMutableDictionary *pointerObjDict;
|
NSMutableDictionary *pointerObjDict;
|
||||||
|
|
||||||
CGFloat slideGestureVerticalThreshold;
|
CGFloat slideGestureVerticalThreshold;
|
||||||
CGFloat screenWidthWithThreshold;
|
CGFloat screenWidthWithThreshold;
|
||||||
CGFloat EDGE_TOLERANCE;
|
CGFloat _edgeTolerance;
|
||||||
|
|
||||||
CGRect streamViewBounds;
|
CGRect streamViewBounds;
|
||||||
|
|
||||||
|
int64_t moveEventIntervalNSec;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings{
|
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings{
|
||||||
self = [super init];
|
self = [super init];
|
||||||
self->streamView = view;
|
self->streamView = view;
|
||||||
self->currentSettings = settings;
|
|
||||||
self->activateCoordSelector = currentSettings.pointerVelocityModeDivider.floatValue != 1.0;
|
self->activateCoordSelector = settings.pointerVelocityModeDivider.floatValue != 1.0;
|
||||||
self->touchMoveEventIntervalUs = currentSettings.touchMoveEventInterval.intValue;
|
self->moveEventIntervalNSec = (int64_t)(settings.touchMoveEventInterval.intValue * 1000);;
|
||||||
self->streamViewBounds = view.bounds;
|
self->streamViewBounds = view.bounds;
|
||||||
|
|
||||||
self->pointerIdDict = [NSMutableDictionary dictionary];
|
self->pointerIdDict = [NSMutableDictionary dictionary];
|
||||||
self->pointerIdPool = [NSMutableSet set];
|
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
|
for (uint8_t i = 0; i <= 10; i++) { //ipadOS supports upto 11 finger touches
|
||||||
[self->pointerIdPool addObject:@(i)];
|
[self->pointerIdPool addObject:@(i)];
|
||||||
|
[self->trackPointPool addObject:[GraphicUtils makeTouchTrackpointIn:streamView]];
|
||||||
}
|
}
|
||||||
|
|
||||||
self->activePointerIds = [NSMutableSet set];
|
self->activePointerIds = [NSMutableSet set];
|
||||||
self->blacklistedTouches = [NSMutableSet set];
|
self->blacklistedTouches = [NSMutableSet set];
|
||||||
|
|
||||||
@@ -81,12 +88,11 @@
|
|||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
self->pointerObjDict = [NSMutableDictionary dictionary];
|
self->pointerObjDict = [NSMutableDictionary dictionary];
|
||||||
|
|
||||||
EDGE_TOLERANCE = 10.0;
|
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
|
||||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
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;
|
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
|
//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];
|
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];
|
[blacklistedTouches addObject:touchAddrObj];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,65 +215,75 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (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){
|
for (UITouch* touch in touches){
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
// 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
|
[self handleTouchDown:touch]; //generate & populate pointerId
|
||||||
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
[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 {
|
- (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){
|
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];
|
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
[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)
|
[[self getPointerObjFromDict:touch] doesNeedResetCoords];
|
||||||
usleep(self->touchMoveEventIntervalUs);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
else {
|
|
||||||
for (UITouch* touch in touches){
|
if(self->trackPointEnabled){
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
[CATransaction begin];
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
[CATransaction setDisableActions:YES];
|
||||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
for (UITouch* touch in touches){
|
||||||
// usleep(self->touchMoveEventIntervalUs);
|
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 {
|
- (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){
|
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 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];
|
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
||||||
[self->blacklistedTouches removeObject:@((uintptr_t)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 {
|
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
@@ -303,5 +319,19 @@
|
|||||||
return pointer.useRelativeCoords ? pointer.latestRelativePoint : pointer.latestPoint;
|
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
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
#import "DataManager.h"
|
#import "DataManager.h"
|
||||||
#import "CustomTapGestureRecognizer.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
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@interface RelativeTouchHandler : UIResponder
|
@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;
|
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// RelativeTouchHandler.m
|
// RelativeTouchHandler.m
|
||||||
// Moonlight
|
// VoidLink
|
||||||
//
|
//
|
||||||
// Completely refactored by True砖家 on 2024.9.13
|
// Completely refactored by True砖家 on 2024.9.13
|
||||||
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
||||||
@@ -23,22 +23,26 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
|||||||
BOOL firstTouchMoved;
|
BOOL firstTouchMoved;
|
||||||
BOOL mousePointerMoved;
|
BOOL mousePointerMoved;
|
||||||
BOOL quickTapDetected;
|
BOOL quickTapDetected;
|
||||||
BOOL isInMouseWheelScrollingMode;
|
|
||||||
|
|
||||||
// upper screen edge check
|
// upper screen edge check
|
||||||
bool touchPointSpawnedAtUpperScreenEdge;
|
bool touchPointSpawnedAtUpperScreenEdge;
|
||||||
CGFloat slideGestureVerticalThreshold;
|
CGFloat slideGestureVerticalThreshold;
|
||||||
CGFloat screenWidthWithThreshold;
|
CGFloat screenWidthWithThreshold;
|
||||||
CGFloat EDGE_TOLERANCE;
|
CGFloat _edgeTolerance;
|
||||||
|
|
||||||
UITouch* touchLockedForMouseMove;
|
UITouch* touchLockedForMouseMove;
|
||||||
|
UITouch* quickTapTouch;
|
||||||
|
|
||||||
|
bool multiTouchesDetected;
|
||||||
|
|
||||||
|
CADisplayLink *displayLink;
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
#if TARGET_OS_TV
|
||||||
UIGestureRecognizer* remotePressRecognizer;
|
UIGestureRecognizer* remotePressRecognizer;
|
||||||
UIGestureRecognizer* remoteLongPressRecognizer;
|
UIGestureRecognizer* remoteLongPressRecognizer;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
StreamView* streamView;
|
__weak StreamView* streamView;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings {
|
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings {
|
||||||
@@ -53,18 +57,21 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
|||||||
_mouseRightClickTapRecognizer.delaysTouchesBegan = NO;
|
_mouseRightClickTapRecognizer.delaysTouchesBegan = NO;
|
||||||
_mouseRightClickTapRecognizer.delaysTouchesEnded = NO;
|
_mouseRightClickTapRecognizer.delaysTouchesEnded = NO;
|
||||||
[self->streamView.streamFrameTopLayerView addGestureRecognizer:_mouseRightClickTapRecognizer]; // add all additional gestures to the streamFrameTopLayerView instead of the streamview.
|
[self->streamView.streamFrameTopLayerView addGestureRecognizer:_mouseRightClickTapRecognizer]; // add all additional gestures to the streamFrameTopLayerView instead of the streamview.
|
||||||
|
_mouseRightClickTapRecognizer.touchCapturingView = streamView;
|
||||||
|
|
||||||
isInMouseWheelScrollingMode = false;
|
|
||||||
firstTouchMoved = false;
|
firstTouchMoved = false;
|
||||||
mousePointerMoved = false;
|
mousePointerMoved = false;
|
||||||
mousePointerTimestamp = 0;
|
mousePointerTimestamp = 0;
|
||||||
|
|
||||||
// upper screen check
|
// upper screen check
|
||||||
EDGE_TOLERANCE = 10.0;
|
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
|
||||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
||||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
|
||||||
self->touchPointSpawnedAtUpperScreenEdge = false;
|
self->touchPointSpawnedAtUpperScreenEdge = false;
|
||||||
|
|
||||||
|
// self->displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
|
||||||
|
// [self->displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
#if TARGET_OS_TV
|
||||||
remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)];
|
remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)];
|
||||||
remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
|
remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
|
||||||
@@ -81,42 +88,21 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
|||||||
|
|
||||||
- (bool)isOnScreenControllerBeingPressed:(NSSet* )touches{
|
- (bool)isOnScreenControllerBeingPressed:(NSSet* )touches{
|
||||||
for(UITouch* touch in touches){
|
for(UITouch* touch in touches){
|
||||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) return true;
|
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) return true;
|
||||||
}
|
}
|
||||||
return false;
|
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 {
|
- (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), ^{
|
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);
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||||
|
Log(LOG_D, @"Sending right mouse button press");
|
||||||
// Wait 100 ms to simulate a real button press
|
// Wait 100 ms to simulate a real button press
|
||||||
usleep(100 * 1000);
|
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
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;
|
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 {
|
- (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
|
//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];
|
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;
|
self->touchPointSpawnedAtUpperScreenEdge = true;
|
||||||
return;
|
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.
|
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]]){
|
if([UITouchUtil touchesIn:streamView from:event].count>=2){
|
||||||
NSLog(@"get in scrolling mode");
|
multiTouchesDetected = true;
|
||||||
isInMouseWheelScrollingMode = true;
|
return;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NSLog(@"touches count in began stage: %llu", (uint64_t)[touches count]);
|
|
||||||
|
|
||||||
UITouch* candidateTouch = nil;
|
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 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.
|
// the onscreen button are UIViews, they intercept UITouch events, so we don't need to worry about them.
|
||||||
for(UITouch* touch in touches){
|
for(UITouch* touch in touches){
|
||||||
// NSLog(@"candidate touch test: %llu", (uint64_t)touch);
|
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]){
|
||||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]){
|
|
||||||
// NSLog(@"%f controller tap detected", CACurrentMediaTime());
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
else candidateTouch = touch;
|
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
|
// quick double tap detection for dragging. simulates a real notebook computer touchpad
|
||||||
CGPoint currentTouchLocation = [candidateTouch locationInView:streamView];
|
CGPoint currentTouchLocation = [candidateTouch locationInView:streamView];
|
||||||
NSTimeInterval tapInterval = CACurrentMediaTime() - mousePointerTimestamp;
|
|
||||||
if(tapInterval < QUICK_TAP_TIME_INTERVAL && [self isAdjacentTouches:currentTouchLocation from:initialMousePointerLocation] ) {
|
if([UITouchUtil touchesIn:streamView from:event].count == 1){
|
||||||
// NSLog(@"quick click detected");
|
NSTimeInterval tapInterval = CACurrentMediaTime() - mousePointerTimestamp;
|
||||||
quickTapDetected = true;
|
if(tapInterval < QUICK_TAP_TIME_INTERVAL
|
||||||
NSLog(@"quick Tap Detected");
|
&& [self isAdjacentTouches:currentTouchLocation from:initialMousePointerLocation]) {
|
||||||
// LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); // do not press down mouse button here, or it wiil easily turn to double click on remote PC
|
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]){
|
if(candidateTouch != nil && ![[event allTouches] containsObject:touchLockedForMouseMove]){
|
||||||
touchLockedForMouseMove = candidateTouch;
|
touchLockedForMouseMove = candidateTouch;
|
||||||
// NSLog(@"Candidate touch for mouse movement locked");
|
// NSLog(@"Candidate touch for mouse movement locked");
|
||||||
@@ -184,37 +172,37 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
|
|
||||||
//NSLog(@"%f, touchesMoved callback, is scrolling: %d, touches count: %d", CACurrentMediaTime(), isInMouseWheelScrollingMode, (uint32_t)[touches count]);
|
|
||||||
|
|
||||||
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];
|
|
||||||
|
|
||||||
CGPoint avgLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2);
|
NSSet* currentTouches = [UITouchUtil touchesIn:streamView from:event];
|
||||||
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
|
if(![self isOnScreenControllerBeingPressed:currentTouches]) [TouchPadGestureHandler handleGestureIn:streamView with:event];
|
||||||
LiSendHighResScrollEvent((avgLocation.y - twoFingerTouchLocation.y) * 10);
|
|
||||||
}
|
if(multiTouchesDetected) return;
|
||||||
twoFingerTouchLocation = avgLocation;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// NSLog(@"%f touchesMoved callback, locked touch: %llu", CACurrentMediaTime(), (uintptr_t)touchLockedForMouseMove);
|
|
||||||
|
|
||||||
if([touches containsObject:touchLockedForMouseMove]){
|
if([touches containsObject:touchLockedForMouseMove]){
|
||||||
mousePointerMoved = true;
|
CGPoint currentLocation = [touchLockedForMouseMove locationInView:streamView];
|
||||||
[self sendMouseMoveEvent:touchLockedForMouseMove];
|
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 {
|
- (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;
|
return;
|
||||||
}
|
}
|
||||||
|
if([UITouchUtil touchesIn:streamView from:event].count == touches.count) multiTouchesDetected = false;
|
||||||
|
|
||||||
if([touches containsObject:touchLockedForMouseMove]){
|
if([touches containsObject:touchLockedForMouseMove]){
|
||||||
// dealing with a single first tap, whether the button will be released, is going to be decided in sendLongMouseLeftButtonClickEvent
|
// dealing with a single first tap, whether the button will be released, is going to be decided in sendLongMouseLeftButtonClickEvent
|
||||||
if(!mousePointerMoved && !self->quickTapDetected) [self sendLongMouseLeftButtonClickEvent];
|
if(!mousePointerMoved && !self->quickTapDetected) [self sendLongMouseLeftButtonClickEvent];
|
||||||
@@ -230,24 +218,23 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
|||||||
mousePointerMoved = false;
|
mousePointerMoved = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if([[event allTouches] count] == [touches count]){
|
for(UITouch* touch in touches){
|
||||||
isInMouseWheelScrollingMode = false;
|
[OnScreenControls.touchesCapturedByOnScreenControls removeObject:touch];
|
||||||
|
}
|
||||||
|
|
||||||
|
if([UITouchUtil touchesIn:streamView from:event].count == [touches count]){
|
||||||
touchLockedForMouseMove = nil;
|
touchLockedForMouseMove = nil;
|
||||||
mousePointerMoved = false; // need to reset this anyway
|
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;
|
touchPointSpawnedAtUpperScreenEdge = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)sendMouseMoveEvent:(CGPoint)currentLocation{
|
||||||
- (void)sendMouseMoveEvent:(UITouch* )touch{
|
|
||||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
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 && !isAdjacentPoints) {
|
||||||
|
|
||||||
if (!self->firstTouchMoved) {
|
|
||||||
self->latestMousePointerLocation = currentLocation;
|
self->latestMousePointerLocation = currentLocation;
|
||||||
self->firstTouchMoved = true;
|
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;
|
int deltaY = (currentLocation.y - self->latestMousePointerLocation.y) * 1.35 * self->currentSettings.mousePointerVelocityFactor.floatValue;
|
||||||
|
|
||||||
if (deltaX != 0 || deltaY != 0) {
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
LiSendMouseMoveEvent(deltaX, deltaY);
|
|
||||||
self->latestMousePointerLocation = currentLocation;
|
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...
|
// this will turn into a dragging anytime...
|
||||||
- (void)sendLongMouseLeftButtonClickEvent{
|
- (void)sendLongMouseLeftButtonClickEvent{
|
||||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||||
// if (!self->isDragging){
|
// if (!self->isDragging){
|
||||||
Log(LOG_D, @"Sending left mouse button press");
|
Log(LOG_D, @"Sending left mouse button press");
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||||
|
|
||||||
// Wait 100 ms to simulate a real button press
|
// Wait 100 ms to simulate a real button press
|
||||||
usleep(QUICK_TAP_TIME_INTERVAL * 1000000);
|
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(QUICK_TAP_TIME_INTERVAL * NSEC_PER_SEC));
|
||||||
if(!self->quickTapDetected){
|
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
if(!self->quickTapDetected){
|
||||||
}
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||||
// else NSLog(@"Left mouse button release cancelled, keep pressing down, turning into dragging...");
|
}
|
||||||
|
// 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.
|
// do not release the button if we're still dragging, this will prevent the dragging from being interrupted.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)sendShortMouseLeftButtonClickEvent{
|
- (void)sendShortMouseLeftButtonClickEvent{
|
||||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC));
|
||||||
usleep(50 * 1000);
|
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||||
usleep(50 * 1000);
|
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
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)toggleStatsOverlay;
|
||||||
- (void)toggleMouseCapture;
|
- (void)toggleMouseCapture;
|
||||||
- (void)toggleMouseVisible;
|
- (void)toggleMouseVisible;
|
||||||
|
- (void)disconnectAndQuitApp;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -33,18 +34,24 @@
|
|||||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate>
|
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate>
|
||||||
#endif
|
#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) CGFloat streamAspectRatio;
|
||||||
@property (assign, nonatomic) CGRect originalFrame;
|
@property (assign, nonatomic) CGRect originalFrame;
|
||||||
@property (assign, nonatomic) bool widgetToolOpened;
|
@property (assign, nonatomic) bool widgetToolOpened;
|
||||||
|
@property (strong, nonatomic) OnScreenControls* onScreenControls;
|
||||||
|
@property (weak, nonatomic) PencilHandler* pencilHandler;
|
||||||
|
@property (weak, nonatomic) UIViewController* streamFrameVC;
|
||||||
|
|
||||||
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
||||||
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
|
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
|
||||||
config:(StreamConfiguration*)streamConfig
|
config:(StreamConfiguration*)streamConfig
|
||||||
streamFrameTopLayerView:(UIView* )topLayerView
|
streamFrameTopLayerView:(UIView* )topLayerView
|
||||||
;
|
;
|
||||||
- (void) showOnScreenControls;
|
|
||||||
|
- (void)cleanUp;
|
||||||
|
|
||||||
|
- (void) reloadLegacyWidgets:(OSCProfile* )profile;
|
||||||
- (void) setOnScreenControls;
|
- (void) setOnScreenControls;
|
||||||
- (void) disableOnScreenControls;
|
- (void) disableOnScreenControls;
|
||||||
- (void) reloadOnScreenControlsRealtimeWith:(ControllerSupport*)controllerSupport
|
- (void) reloadOnScreenControlsRealtimeWith:(ControllerSupport*)controllerSupport
|
||||||
@@ -52,7 +59,9 @@
|
|||||||
- (void) reloadOnScreenControlsWith:(ControllerSupport*)controllerSupport
|
- (void) reloadOnScreenControlsWith:(ControllerSupport*)controllerSupport
|
||||||
andConfig:(StreamConfiguration*)streamConfig;
|
andConfig:(StreamConfiguration*)streamConfig;
|
||||||
- (void) clearOnScreenWidgets;
|
- (void) clearOnScreenWidgets;
|
||||||
- (void) reloadOnScreenWidgetViews;
|
- (void) reloadOnScreenWidgetViews:(bool)reload;
|
||||||
|
- (void) saveStreamViewWidgetChanges;
|
||||||
|
- (bool) isOnScreenWidgetEnabled;
|
||||||
|
|
||||||
- (CGSize) getVideoAreaSize;
|
- (CGSize) getVideoAreaSize;
|
||||||
- (CGPoint) adjustCoordinatesForVideoArea:(CGPoint)point;
|
- (CGPoint) adjustCoordinatesForVideoArea:(CGPoint)point;
|
||||||
@@ -60,11 +69,17 @@
|
|||||||
|
|
||||||
- (OnScreenControlsLevel) getCurrentOscState;
|
- (OnScreenControlsLevel) getCurrentOscState;
|
||||||
|
|
||||||
-(void)readyToBringUpSoftKeyboardByToolbox;
|
- (void)readyToBringUpSoftKeyboardByToolbox;
|
||||||
- (void)keyboardWillShow:(NSNotification *)notification;
|
- (void)keyboardWillShow:(NSNotification *)notification;
|
||||||
- (void)keyboardWillHide;
|
- (void)keyboardWillHide;
|
||||||
- (void)liftMetalVideoViewIfNeeded:(CGFloat)liftHeight;
|
- (void)liftMetalVideoViewIfNeeded:(CGFloat)liftHeight;
|
||||||
|
|
||||||
|
- (void)alterAbsTouchDragWith:(int32_t)mouseButton;
|
||||||
|
|
||||||
|
- (void)enablePencilHover;
|
||||||
|
- (void)disablePencilHover;
|
||||||
|
- (void)setAllowSingleTouchEnabled:(BOOL)enabled;
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
#if !TARGET_OS_TV
|
||||||
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
|
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
#import "KeyboardInputField.h"
|
#import "KeyboardInputField.h"
|
||||||
#import "CustomTapGestureRecognizer.h"
|
#import "CustomTapGestureRecognizer.h"
|
||||||
#import "LocalizationHelper.h"
|
#import "LocalizationHelper.h"
|
||||||
|
#import "StreamFrameViewController.h"
|
||||||
|
|
||||||
|
|
||||||
static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||||
@@ -34,17 +35,14 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
Stream Video has been moved out of this class to _renderView in StreamFrameViewController.
|
Stream Video has been moved out of this class to _renderView in StreamFrameViewController.
|
||||||
*/
|
*/
|
||||||
@implementation StreamView {
|
@implementation StreamView {
|
||||||
UIView* streamFrameTopLayerView;
|
|
||||||
TemporarySettings* settings;
|
TemporarySettings* settings;
|
||||||
|
|
||||||
OnScreenControls* onScreenControls;
|
|
||||||
|
|
||||||
KeyboardInputField* keyInputField;
|
KeyboardInputField* keyInputField;
|
||||||
BOOL isInputingText;
|
BOOL isInputingText;
|
||||||
|
bool isPencilHovering;
|
||||||
NSMutableSet* keysDown;
|
NSMutableSet* keysDown;
|
||||||
float streamAspectRatio;
|
float streamAspectRatio;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// iOS 13.4 mouse support
|
// iOS 13.4 mouse support
|
||||||
NSInteger lastMouseButtonMask;
|
NSInteger lastMouseButtonMask;
|
||||||
@@ -60,8 +58,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
int localMousePointerMode;
|
int localMousePointerMode;
|
||||||
|
|
||||||
UIResponder* touchHandler;
|
UIResponder* touchHandler;
|
||||||
|
|
||||||
id<UserInteractionDelegate> interactionDelegate;
|
|
||||||
NSTimer* interactionTimer;
|
NSTimer* interactionTimer;
|
||||||
BOOL hasUserInteracted;
|
BOOL hasUserInteracted;
|
||||||
|
|
||||||
@@ -76,6 +73,9 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
UILabel* keyboardToggleTip;
|
UILabel* keyboardToggleTip;
|
||||||
|
|
||||||
UIKeyModifierFlags comboKeyModifierFlags;
|
UIKeyModifierFlags comboKeyModifierFlags;
|
||||||
|
|
||||||
|
WidgetSizeTransition _widgetSizeTransition;
|
||||||
|
OSCProfilesManager* oscProfileMan;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
||||||
@@ -84,11 +84,11 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
streamFrameTopLayerView:(UIView* )topLayerView{
|
streamFrameTopLayerView:(UIView* )topLayerView{
|
||||||
self->comboKeyModifierFlags = (UIKeyModifierControl|UIKeyModifierAlternate|UIKeyModifierShift);
|
self->comboKeyModifierFlags = (UIKeyModifierControl|UIKeyModifierAlternate|UIKeyModifierShift);
|
||||||
|
|
||||||
self->streamFrameTopLayerView = topLayerView;
|
self->_streamFrameTopLayerView = topLayerView;
|
||||||
self.streamFrameTopLayerView = topLayerView; // this will be used as a read-only pointer for other class
|
self->_interactionDelegate = interactionDelegate;
|
||||||
self->interactionDelegate = interactionDelegate;
|
|
||||||
self->streamAspectRatio = (float)streamConfig.width / (float)streamConfig.height;
|
self->streamAspectRatio = (float)streamConfig.width / (float)streamConfig.height;
|
||||||
self.streamAspectRatio = self->streamAspectRatio;
|
self.streamAspectRatio = self->streamAspectRatio;
|
||||||
|
_widgetSizeTransition = keepWidgetSize;
|
||||||
|
|
||||||
settings = [[[DataManager alloc] init] getSettings];
|
settings = [[[DataManager alloc] init] getSettings];
|
||||||
|
|
||||||
@@ -115,7 +115,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
keyboardToggleTip.numberOfLines = 1;
|
keyboardToggleTip.numberOfLines = 1;
|
||||||
keyboardToggleTip.layer.cornerRadius = 10;
|
keyboardToggleTip.layer.cornerRadius = 10;
|
||||||
keyboardToggleTip.clipsToBounds = true;
|
keyboardToggleTip.clipsToBounds = true;
|
||||||
NSLog(@"setup streamView %f", CACurrentMediaTime());
|
|
||||||
|
|
||||||
// if(settings.touchMode.intValue == NativeTouchOnly) [self addGestureRecognizer:keyboardToggleRecognizer]; //keep legacy approach in pure native mode
|
// 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
|
// 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
|
// tvOS requires RelativeTouchHandler to manage Apple Remote input
|
||||||
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
|
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
|
||||||
#else
|
#else
|
||||||
|
|
||||||
|
PencilHandler.shared = [[PencilHandler alloc] initWithStreamView:self settings:settings];
|
||||||
|
_pencilHandler = PencilHandler.shared;
|
||||||
|
|
||||||
// iOS uses touch Mode depending on user preference
|
// iOS uses touch Mode depending on user preference
|
||||||
|
|
||||||
switch (settings.touchMode.intValue) {
|
switch (settings.touchMode.intValue) {
|
||||||
case NativeTouch:
|
case NativeTouch:
|
||||||
keyboardToggleRecognizer.immediateTriggering = false;
|
keyboardToggleRecognizer.immediateTriggering = false;
|
||||||
self->touchHandler = [[NativeTouchHandler alloc] initWithView:self andSettings:settings];break;
|
self->touchHandler = [[NativeTouchHandler alloc] initWithView:self andSettings:settings];
|
||||||
|
break;
|
||||||
case NativeTouchOnly:
|
case NativeTouchOnly:
|
||||||
keyboardToggleRecognizer.immediateTriggering = false;
|
keyboardToggleRecognizer.immediateTriggering = false;
|
||||||
self->touchHandler = [[PureNativeTouchHandler alloc] initWithView:self andSettings:settings];break;
|
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.
|
// 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;
|
break;
|
||||||
case AbsoluteTouch:
|
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
|
keyboardToggleRecognizer.immediateTriggering = true; //triggers signal in touchesBegan callback stage
|
||||||
break;
|
break;
|
||||||
|
case TouchDisabled:
|
||||||
|
self->touchHandler = nil;
|
||||||
|
keyboardToggleRecognizer.immediateTriggering = false;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we'll render on-screen controls on the toplayer too:
|
// 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
|
// here we pass the tap recognizer to the onscreencontrols obj
|
||||||
if (settings.touchMode.intValue == RelativeTouch){
|
if (settings.touchMode.intValue == RelativeTouch){
|
||||||
@@ -161,10 +170,10 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue];
|
OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue];
|
||||||
if (settings.touchMode.intValue != RelativeTouch && settings.touchMode.intValue != NativeTouch ) {
|
if (settings.touchMode.intValue != RelativeTouch && settings.touchMode.intValue != NativeTouch ) {
|
||||||
Log(LOG_I, @"On-screen controls disabled in non-relative touch mode");
|
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
|
//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){
|
if(settings.touchMode.intValue == NativeTouch){
|
||||||
@@ -176,11 +185,11 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
}
|
}
|
||||||
//else if (level == OnScreenControlsLevelAuto) {
|
//else if (level == OnScreenControlsLevelAuto) {
|
||||||
else if (false) { // level auto cancelled in settings
|
else if (false) { // level auto cancelled in settings
|
||||||
[controllerSupport initAutoOnScreenControlMode:onScreenControls];
|
[controllerSupport initAutoOnScreenControlMode:_onScreenControls];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Log(LOG_I, @"Setting manual on-screen controls level: %d", (int)level);
|
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
|
// 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
|
// 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.allowedScrollTypesMask = UIScrollTypeMaskContinuous;
|
||||||
continuousMouseWheelRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)];
|
continuousMouseWheelRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)];
|
||||||
[self addGestureRecognizer:continuousMouseWheelRecognizer];
|
[self addGestureRecognizer:continuousMouseWheelRecognizer];
|
||||||
|
|
||||||
|
isPencilHovering = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#if defined(__IPHONE_16_1) || defined(__TVOS_16_1)
|
#if defined(__IPHONE_16_1) || defined(__TVOS_16_1)
|
||||||
if (@available(iOS 16.1, *)) {
|
if (@available(iOS 16.1, *)) {
|
||||||
UIHoverGestureRecognizer *stylusHoverRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(sendStylusHoverEvent:)];
|
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.delaysTouchesBegan = NO;
|
||||||
keyboardToggleRecognizer.delaysTouchesEnded = NO;
|
keyboardToggleRecognizer.delaysTouchesEnded = NO;
|
||||||
[self->_streamFrameTopLayerView addGestureRecognizer:keyboardToggleRecognizer];
|
[self->_streamFrameTopLayerView addGestureRecognizer:keyboardToggleRecognizer];
|
||||||
|
keyboardToggleRecognizer.touchCapturingView = self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)keyboardWillShow:(NSNotification *)notification{
|
- (void)keyboardWillShow:(NSNotification *)notification{
|
||||||
// NSLog(@"keyboard will show markmark %f", CACurrentMediaTime());
|
// NSLog(@"keyboard will show markmark %f", CACurrentMediaTime());
|
||||||
if(settings.liftStreamViewForKeyboard && !isInputingText){
|
if(settings.liftStreamViewForKeyboard && !isInputingText){
|
||||||
|
isInputingText = true;
|
||||||
|
|
||||||
NSDictionary *userInfo = notification.userInfo;
|
NSDictionary *userInfo = notification.userInfo;
|
||||||
// Get the keyboard size from the notification
|
// Get the keyboard size from the notification
|
||||||
CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||||
// NSLog(@"keyboard will show markmark, lowest height %f", keyboardToggleRecognizer.lowestTouchPointHeight);
|
CGFloat screenHeight = CGRectGetHeight([[UIScreen mainScreen] bounds]);
|
||||||
if(keyboardFrame.size.height < CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.25) return; // return in case of abnormal keyboard height
|
CGFloat keyboardHeight = keyboardFrame.size.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 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.
|
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;
|
CGRect liftedStreamFrame = self.frame;
|
||||||
liftedStreamFrame.origin.y -= HeightViewLiftedTo;
|
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
|
// Also lift Metal video view if using Metal rendering backend
|
||||||
[self liftMetalVideoViewIfNeeded:HeightViewLiftedTo];
|
[self liftMetalVideoViewIfNeeded:HeightViewLiftedTo];
|
||||||
|
|
||||||
isInputingText = true;
|
|
||||||
[self refreshKeyboardToggleRecognizer:settings.keyboardToggleFingers.intValue];
|
[self refreshKeyboardToggleRecognizer:settings.keyboardToggleFingers.intValue];
|
||||||
[keyboardToggleTip removeFromSuperview];
|
[keyboardToggleTip removeFromSuperview];
|
||||||
}
|
}
|
||||||
NSLog(@"keyboard will show %f", CACurrentMediaTime());
|
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
|
// this method also deals with recovering streamview when local keyboard is turned off
|
||||||
- (void)keyboardWillHide{
|
- (void)keyboardWillHide{
|
||||||
// NSLog(@"keyboard will hide markmark %f", CACurrentMediaTime());
|
// NSLog(@"keyboard will hide markmark %f", CACurrentMediaTime());
|
||||||
@@ -329,6 +370,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
if (isInputingText) {
|
if (isInputingText) {
|
||||||
Log(LOG_D, @"Closing the keyboard");
|
Log(LOG_D, @"Closing the keyboard");
|
||||||
[keyInputField resignFirstResponder];
|
[keyInputField resignFirstResponder];
|
||||||
|
[keyboardToggleTip removeFromSuperview];
|
||||||
} else {
|
} else {
|
||||||
Log(LOG_D, @"Opening the keyboard");
|
Log(LOG_D, @"Opening the keyboard");
|
||||||
// Prepare the textbox used to capture keyboard events.
|
// 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
|
#if !TARGET_OS_TV
|
||||||
// Prepare the toolbar above the keyboard for more options
|
// Prepare the toolbar above the keyboard for more options
|
||||||
if(settings.showKeyboardToolbar){
|
if(settings.showKeyboardToolbar){
|
||||||
UIToolbar *customToolbarView = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, 44)];
|
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];
|
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];
|
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];
|
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];
|
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];
|
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];
|
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];
|
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];
|
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];
|
UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
|
||||||
|
|
||||||
[customToolbarView setItems:[NSArray arrayWithObjects:doneBarButton, windowsBarButton, escapeBarButton, tabBarButton, shiftBarButton, controlBarButton, altBarButton, deleteBarButton, flexibleSpace, 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;
|
keyInputField.inputAccessoryView = customToolbarView;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -377,7 +436,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
|
|
||||||
// Notify the delegate if this was a new user interaction
|
// Notify the delegate if this was a new user interaction
|
||||||
if (!timerAlreadyRunning) {
|
if (!timerAlreadyRunning) {
|
||||||
[interactionDelegate userInteractionBegan];
|
[_interactionDelegate userInteractionBegan];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +444,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
if (!hasUserInteracted) {
|
if (!hasUserInteracted) {
|
||||||
// User has finished touching the screen
|
// User has finished touching the screen
|
||||||
interactionTimer = nil;
|
interactionTimer = nil;
|
||||||
[interactionDelegate userInteractionEnded];
|
[_interactionDelegate userInteractionEnded];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// User is still touching the screen. Restart the timer.
|
// 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 !TARGET_OS_TV
|
||||||
if(!_widgetToolOpened) [onScreenControls show];
|
if(!_widgetToolOpened) [_onScreenControls showLegacyWidgetsWith:profile];
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,34 +464,35 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
|
|
||||||
- (void) disableOnScreenControls {
|
- (void) disableOnScreenControls {
|
||||||
#if !TARGET_OS_TV
|
#if !TARGET_OS_TV
|
||||||
[onScreenControls setLevel:OnScreenControlsLevelOff];
|
[_onScreenControls setLevel:OnScreenControlsLevelOff];
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// we'll enable on screen buttons, and disable on screen controllers for absolute touch
|
// we'll enable on screen buttons, and disable on screen controllers for absolute touch
|
||||||
- (bool) isOscEnabled{
|
- (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
|
// we'll enable on screen buttons, and disable on screen controllers for absolute touch
|
||||||
- (bool) isOnScreenButtonEnabled{
|
- (bool) isOnScreenWidgetEnabled{
|
||||||
return (settings.touchMode.intValue == RelativeTouch || settings.touchMode.intValue == NativeTouch || settings.touchMode.intValue == AbsoluteTouch) && settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
|
return [self isOscEnabled] && settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) reloadOnScreenControlsRealtimeWith:(ControllerSupport*)controllerSupport
|
- (void) reloadOnScreenControlsRealtimeWith:(ControllerSupport*)controllerSupport
|
||||||
andConfig:(StreamConfiguration*)streamConfig {
|
andConfig:(StreamConfiguration*)streamConfig {
|
||||||
[self reloadOnScreenControlsWith:controllerSupport andConfig: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
|
- (void) reloadOnScreenControlsWith:(ControllerSupport*)controllerSupport
|
||||||
andConfig:(StreamConfiguration*)streamConfig {
|
andConfig:(StreamConfiguration*)streamConfig {
|
||||||
|
|
||||||
// we'll render on-screen controllers on the toplayer too.
|
// 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:
|
// pass mouseRightClickTapRecognizer to onScreenControls obj here:
|
||||||
if([self isOscEnabled]){
|
if([self isOscEnabled]){
|
||||||
@@ -440,12 +500,14 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
RelativeTouchHandler* relativeTouchHandler = (RelativeTouchHandler *)touchHandler;
|
RelativeTouchHandler* relativeTouchHandler = (RelativeTouchHandler *)touchHandler;
|
||||||
onScreenControls.mouseRightClickTapRecognizer = relativeTouchHandler.mouseRightClickTapRecognizer;
|
onScreenControls.mouseRightClickTapRecognizer = relativeTouchHandler.mouseRightClickTapRecognizer;
|
||||||
} */
|
} */
|
||||||
if([self isOscEnabled]) [onScreenControls setLevel:(OnScreenControlsLevel)settings.onscreenControls.intValue];
|
if([self isOscEnabled]) [_onScreenControls setLevel:(OnScreenControlsLevel)settings.onscreenControls.intValue];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void) clearOnScreenWidgets{
|
- (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]]) {
|
if ([subview isKindOfClass:[OnScreenWidgetView class]]) {
|
||||||
// 如果是,就添加到将要被移除的数组中
|
// 如果是,就添加到将要被移除的数组中
|
||||||
@@ -456,8 +518,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
|
|
||||||
- (CGPoint)denormalizeWidgetPosition:(CGPoint)position {
|
- (CGPoint)denormalizeWidgetPosition:(CGPoint)position {
|
||||||
if(position.x < 1.0 && position.y < 1.0){
|
if(position.x < 1.0 && position.y < 1.0){
|
||||||
position.x = position.x * streamFrameTopLayerView.bounds.size.width;
|
position.x = position.x * _streamFrameTopLayerView.bounds.size.width;
|
||||||
position.y = position.y * streamFrameTopLayerView.bounds.size.height;
|
position.y = position.y * _streamFrameTopLayerView.bounds.size.height;
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
NSLog(@"invalid coords");
|
NSLog(@"invalid coords");
|
||||||
@@ -465,59 +527,203 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) reloadOnScreenWidgetViews{
|
- (WidgetSizeReference)getCurrentWidgetSizeReference{
|
||||||
|
if(_widgetSizeTransition == keepWidgetSize) return longSide;
|
||||||
// NSLog(@"reload on screen keyboard buttons here");
|
else if(_widgetSizeTransition == transitionWithOrientation) return self.bounds.size.width > self.bounds.size.height ? longSide : shortSide;
|
||||||
|
else return longSide;
|
||||||
|
}
|
||||||
|
|
||||||
// remove all keyboard widget views first
|
- (void)saveStreamViewWidgetChanges{
|
||||||
[self clearOnScreenWidgets];
|
/*
|
||||||
|
NSMutableDictionary* relocatedWidgetDict = [NSMutableDictionary dictionary];
|
||||||
|
|
||||||
// bool customOscEnabled = [self isOscEnabled] && settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
|
for(UIView* subview in self->_streamFrameTopLayerView.subviews){
|
||||||
|
if([subview isKindOfClass:[OnScreenWidgetView class]]){
|
||||||
if(![self isOnScreenButtonEnabled]) return;
|
OnScreenWidgetView* widget = (OnScreenWidgetView* )subview;
|
||||||
|
if(widget.relocatedDuringStreaming) [relocatedWidgetDict setObject:widget forKey:@(widget.sequence)];
|
||||||
OSCProfilesManager* profilesManager = [OSCProfilesManager sharedManager: self.bounds];
|
OnScreenWidgetView* widget = [OnScreenWidgetView widgetForKey:newButtonState.sequence];
|
||||||
OSCProfile *oscProfile = [profilesManager getSelectedProfile]; //returns the currently selected OSCProfile
|
if(widget.isFolder)
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 {
|
- (OnScreenControlsLevel) getCurrentOscState {
|
||||||
if (onScreenControls == nil) {
|
if (_onScreenControls == nil) {
|
||||||
return OnScreenControlsLevelOff;
|
return OnScreenControlsLevelOff;
|
||||||
}
|
}
|
||||||
else {
|
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]];
|
CGPoint location = [self adjustCoordinatesForVideoArea:[event locationInView:self]];
|
||||||
CGSize videoSize = [self getVideoAreaSize];
|
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,
|
return LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height,
|
||||||
(event.force / event.maximumPossibleForce) / sin(event.altitudeAngle),
|
(event.force / event.maximumPossibleForce) / sin(event.altitudeAngle),
|
||||||
0.0f, 0.0f,
|
0.0f, 0.0f,
|
||||||
@@ -624,20 +832,31 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
|
|
||||||
- (void)sendStylusHoverEvent:(UIHoverGestureRecognizer*)gesture API_AVAILABLE(ios(13.0)) {
|
- (void)sendStylusHoverEvent:(UIHoverGestureRecognizer*)gesture API_AVAILABLE(ios(13.0)) {
|
||||||
uint8_t type;
|
uint8_t type;
|
||||||
|
|
||||||
switch (gesture.state) {
|
switch (gesture.state) {
|
||||||
case UIGestureRecognizerStateBegan:
|
case UIGestureRecognizerStateBegan:
|
||||||
case UIGestureRecognizerStateChanged:
|
case UIGestureRecognizerStateChanged:
|
||||||
type = LI_TOUCH_EVENT_HOVER;
|
type = LI_TOUCH_EVENT_HOVER;
|
||||||
|
isPencilHovering = true;
|
||||||
|
if(!PencilHandler.hoverSupported) PencilHandler.hoverSupported = true;
|
||||||
|
// if(gesture.state == UIGestureRecognizerStateChanged) NSLog(@"UIGestureRecognizerStateChanged %f", CACurrentMediaTime());
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case UIGestureRecognizerStateEnded:
|
case UIGestureRecognizerStateEnded:
|
||||||
type = LI_TOUCH_EVENT_HOVER_LEAVE;
|
type = LI_TOUCH_EVENT_HOVER_LEAVE;
|
||||||
|
// NSLog(@"UIGestureRecognizerStateEnded %f", CACurrentMediaTime());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return;
|
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]];
|
CGPoint location = [self adjustCoordinatesForVideoArea:[gesture locationInView:self]];
|
||||||
CGSize videoSize = [self getVideoAreaSize];
|
CGSize videoSize = [self getVideoAreaSize];
|
||||||
@@ -658,8 +877,26 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
}
|
}
|
||||||
#endif
|
#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
|
#endif
|
||||||
@@ -672,13 +909,18 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
[touchHandler touchesBegan:touches withEvent:event];
|
[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.
|
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
|
NSMutableSet* pencilTouches = [NSMutableSet set];
|
||||||
for (UITouch* touch in touches) {
|
for (UITouch* touch in touches) {
|
||||||
if (touch.type == UITouchTypePencil) {
|
if (touch.type == UITouchTypePencil) {
|
||||||
if ([self sendStylusEvent:touch]) return;
|
[pencilTouches addObject:touch];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
NSMutableSet* nonPencilTouches;
|
||||||
|
if(pencilTouches.count>0){
|
||||||
|
[_pencilHandler touchesBegan:pencilTouches withEvent:event];
|
||||||
|
nonPencilTouches = [touches mutableCopy];
|
||||||
|
[nonPencilTouches minusSet:pencilTouches];
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
if ([self handleMouseButtonEvent:BUTTON_ACTION_PRESS
|
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
|
// Notify of user interaction and start expiration timer
|
||||||
[self startInteractionTimer];
|
[self startInteractionTimer];
|
||||||
|
|
||||||
|
NSSet* targetTouches = nonPencilTouches ? nonPencilTouches : touches;
|
||||||
if(settings.touchMode.intValue == NativeTouch || settings.touchMode.intValue == RelativeTouch){
|
if(settings.touchMode.intValue == NativeTouch || settings.touchMode.intValue == RelativeTouch){
|
||||||
[self->onScreenControls handleTouchDownEvent:touches];
|
[self->_onScreenControls handleTouchDownEvent:targetTouches];
|
||||||
[self->touchHandler touchesBegan:touches withEvent:event];
|
[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];
|
UIImage *image = [UIImage imageNamed:imageName];
|
||||||
|
|
||||||
|
|
||||||
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
|
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||||
|
// [button setTitle:@"666" forState:UIControlStateNormal];
|
||||||
[button setImage:image 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.contentMode = UIViewContentModeScaleAspectFit;
|
||||||
button.imageView.backgroundColor = backgroundColor;
|
button.imageView.backgroundColor = backgroundColor;
|
||||||
button.imageView.layer.cornerRadius = 10.0;
|
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];
|
[button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
|
||||||
objc_setAssociatedObject(button, "keyCode", @(keyCode), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
objc_setAssociatedObject(button, "keyCode", @(keyCode), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||||
objc_setAssociatedObject(button, "isToggleable", @(isToggleable), 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;
|
isOn = !isOn;
|
||||||
// Update the button's appearance based on its new state
|
// Update the button's appearance based on its new state
|
||||||
if (isOn) {
|
if (isOn) {
|
||||||
sender.imageView.backgroundColor = [UIColor lightGrayColor];
|
sender.imageView.backgroundColor = GenericUtils.liquidGlassEnabled ? [UIColor.systemGrayColor colorWithAlphaComponent:0.5] : [UIColor lightGrayColor];
|
||||||
} else {
|
} else {
|
||||||
sender.imageView.backgroundColor = [UIColor blackColor];
|
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 {
|
- (BOOL)handleMouseButtonEvent:(int)buttonAction forTouches:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
|
|
||||||
|
|
||||||
// NSLog(@"mouse click time: %f", CACurrentMediaTime()*1000);
|
|
||||||
#if !TARGET_OS_TV
|
#if !TARGET_OS_TV
|
||||||
if (@available(iOS 13.4, *)) {
|
if (@available(iOS 13.4, *)) {
|
||||||
UITouch* touch = [touches anyObject];
|
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.
|
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) {
|
for (UITouch* touch in touches) {
|
||||||
if (touch.type == UITouchTypePencil) {
|
if (touch.type == UITouchTypePencil) {
|
||||||
if ([self sendStylusEvent:touch]) return;
|
[pencilTouches addObject:touch];
|
||||||
}
|
}
|
||||||
if (@available(iOS 13.4, *)) {
|
if (@available(iOS 13.4, *)) {
|
||||||
UITouch *touch = [touches anyObject];
|
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.
|
// don't require this, but we do it anyway for them too.
|
||||||
// Cursor movement without a button held down is handled
|
// Cursor movement without a button held down is handled
|
||||||
// in pointerInteraction:regionForRequest:defaultRegion.
|
// in pointerInteraction:regionForRequest:defaultRegion.
|
||||||
[self updateCursorLocation:[touch locationInView:self] isMouse:YES];
|
if(!isPencilHovering) [self updateCursorLocation:[touch locationInView:self] isMouse:YES];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
NSMutableSet* nonPencilTouches;
|
||||||
|
if(pencilTouches.count>0){
|
||||||
|
[_pencilHandler touchesMoved:pencilTouches withEvent:event];
|
||||||
|
nonPencilTouches = [touches mutableCopy];
|
||||||
|
[nonPencilTouches minusSet:pencilTouches];
|
||||||
|
}
|
||||||
|
|
||||||
hasUserInteracted = YES;
|
hasUserInteracted = YES;
|
||||||
|
|
||||||
|
NSSet* targetTouches = nonPencilTouches ? nonPencilTouches : touches;
|
||||||
if(self->settings.touchMode.intValue == NativeTouch || self->settings.touchMode.intValue == RelativeTouch){
|
if(self->settings.touchMode.intValue == NativeTouch || self->settings.touchMode.intValue == RelativeTouch){
|
||||||
[self->touchHandler touchesMoved:touches withEvent:event];
|
[self->touchHandler touchesMoved:targetTouches withEvent:event];
|
||||||
[self->onScreenControls handleTouchMovedEvent:touches];
|
[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{
|
- (void) handleKeyCombos:(UIPress*) press{
|
||||||
@@ -871,16 +1126,19 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
}
|
}
|
||||||
switch (press.key.keyCode) {
|
switch (press.key.keyCode) {
|
||||||
case UIKeyboardHIDUsageKeyboardQ:
|
case UIKeyboardHIDUsageKeyboardQ:
|
||||||
[interactionDelegate streamExitRequested];
|
[_interactionDelegate streamExitRequested];
|
||||||
break;
|
break;
|
||||||
case UIKeyboardHIDUsageKeyboardS:
|
case UIKeyboardHIDUsageKeyboardS:
|
||||||
[interactionDelegate toggleStatsOverlay];
|
[_interactionDelegate toggleStatsOverlay];
|
||||||
break;
|
break;
|
||||||
case UIKeyboardHIDUsageKeyboardM:
|
case UIKeyboardHIDUsageKeyboardM:
|
||||||
[interactionDelegate toggleMouseCapture];
|
[_interactionDelegate toggleMouseCapture];
|
||||||
break;
|
break;
|
||||||
case UIKeyboardHIDUsageKeyboardC:
|
case UIKeyboardHIDUsageKeyboardC:
|
||||||
[interactionDelegate toggleMouseVisible];
|
[_interactionDelegate toggleMouseVisible];
|
||||||
|
break;
|
||||||
|
case UIKeyboardHIDUsageKeyboardD:
|
||||||
|
[_interactionDelegate disconnectAndQuitApp];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
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.
|
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
|
NSMutableSet* pencilTouches = [NSMutableSet set];
|
||||||
for (UITouch* touch in touches) {
|
for (UITouch* touch in touches) {
|
||||||
if (touch.type == UITouchTypePencil) {
|
if (touch.type == UITouchTypePencil) {
|
||||||
if ([self sendStylusEvent:touch]) return;
|
[pencilTouches addObject:touch];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
NSMutableSet* nonPencilTouches;
|
||||||
|
if(pencilTouches.count>0){
|
||||||
|
[_pencilHandler touchesEnded:pencilTouches withEvent:event];
|
||||||
|
nonPencilTouches = [touches mutableCopy];
|
||||||
|
[nonPencilTouches minusSet:pencilTouches];
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
if ([self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
|
if ([self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
|
||||||
forTouches:touches
|
forTouches:nonPencilTouches ? nonPencilTouches : touches
|
||||||
withEvent:event]) {
|
withEvent:event]) {
|
||||||
// If it's a mouse event, we're done
|
// If it's a mouse event, we're done
|
||||||
return;
|
return;
|
||||||
@@ -954,22 +1217,21 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
|||||||
|
|
||||||
hasUserInteracted = YES;
|
hasUserInteracted = YES;
|
||||||
|
|
||||||
|
NSSet* targetTouches = nonPencilTouches ? nonPencilTouches : touches;
|
||||||
if(settings.touchMode.intValue == NativeTouch || settings.touchMode.intValue == RelativeTouch){
|
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->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:touches];
|
[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 {
|
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
[touchHandler touchesCancelled:touches withEvent:event];
|
[touchHandler touchesCancelled:touches withEvent:event];
|
||||||
#if !TARGET_OS_TV
|
#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 (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) {
|
||||||
for (UITouch* touch in touches) {
|
if (touch.type == UITouchTypePencil) {
|
||||||
if (touch.type == UITouchTypePencil) {
|
[self touchesEnded:touches withEvent:event];
|
||||||
if ([self sendStylusEvent:touch]) return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#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.
|
// Move the cursor on the host if no buttons are pressed.
|
||||||
// Motion with buttons pressed in handled in touchesMoved:
|
// Motion with buttons pressed in handled in touchesMoved:
|
||||||
if (lastMouseButtonMask == 0) {
|
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
|
// 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 {
|
- (void)onKeyboardPressed:(UITextField *)textField {
|
||||||
|
|
||||||
|
if (textField.markedTextRange) return;
|
||||||
|
|
||||||
NSString* inputText = textField.text;
|
NSString* inputText = textField.text;
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||||
// If the text became empty, we know the user pressed the backspace key.
|
// 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);
|
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
|
#if !TARGET_OS_TV
|
||||||
- (BOOL)isMultipleTouchEnabled {
|
- (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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>GCSupportsGameMode</key>
|
|
||||||
<true/>
|
|
||||||
<key>CADisableMinimumFrameDuration</key>
|
<key>CADisableMinimumFrameDuration</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
@@ -63,22 +61,19 @@
|
|||||||
<string>_nvstream._tcp</string>
|
<string>_nvstream._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<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>
|
<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>
|
<key>UIApplicationSceneManifest</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<true/>
|
||||||
<key>UISceneConfigurations</key>
|
<key>UISceneConfigurations</key>
|
||||||
<dict>
|
<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>
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -88,9 +83,16 @@
|
|||||||
<string>SceneDelegate</string>
|
<string>SceneDelegate</string>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIWindowSceneSessionRoleExternalDisplay</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>External Display Configuration</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>SceneDelegate</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<key>UIApplicationSupportsMultipleScenes</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
</dict>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
@@ -98,6 +100,8 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>audio</string>
|
<string>audio</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIDesignRequiresCompatibility</key>
|
||||||
|
<false/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>Launch Screen</string>
|
<string>Launch Screen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?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">
|
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="hidden" 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"/>
|
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
<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="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="ipv6Address" 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"/>
|
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
<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="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="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="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="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" 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="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="enableGraphs" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="enableHdr" 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="enablePIP" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
<attribute name="enableYUV444" attributeType="Boolean" defaultValueString="NO" 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="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="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="framePacingMode" optional="YES" attributeType="Integer 32" defaultValueString="0" 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="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="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="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="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="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="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="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="mousePointerVelocityFactor" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" 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="onscreenControls" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" 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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="uniqueId" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="unlockDisplayOrientation" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" 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"/>
|
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||||
</entity>
|
</entity>
|
||||||
</model>
|
</model>
|
||||||
@@ -19,25 +19,36 @@
|
|||||||
|
|
||||||
@interface MenuSectionView : UIView
|
@interface MenuSectionView : UIView
|
||||||
|
|
||||||
|
@property (class, nonatomic, assign) BOOL overridePersistedFoldState;
|
||||||
|
|
||||||
// 外部可访问属性
|
// 外部可访问属性
|
||||||
|
@property (nonatomic, strong) UIStackView *rootStackView;
|
||||||
@property (nonatomic, assign) CGFloat leadingTrailingPadding;
|
@property (nonatomic, assign) CGFloat leadingTrailingPadding;
|
||||||
@property (nonatomic, assign) CGFloat separatorLinePadding;
|
@property (nonatomic, assign) CGFloat separatorLinePadding;
|
||||||
|
@property (nonatomic, strong) UILabel *titleLabel;
|
||||||
@property (nonatomic, copy) NSString *sectionTitle;
|
@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, strong) UIColor *backgroundColor;
|
||||||
@property (nonatomic, assign) CGFloat rootStackViewSpacing;
|
@property (nonatomic, assign) CGFloat rootStackViewSpacing;
|
||||||
@property (nonatomic, assign) CGFloat headerViewHeight;
|
@property (nonatomic, assign) CGFloat headerViewHeight;
|
||||||
@property (nonatomic, assign) CGFloat headerViewVerticalSpacing;
|
@property (nonatomic, assign) CGFloat headerViewVerticalSpacing;
|
||||||
@property (nonatomic, strong) NSMutableArray<UIStackView *> *subStackViews;
|
@property (nonatomic, strong) NSMutableArray<UIStackView *> *subStackViews;
|
||||||
|
@property (nonatomic, strong) UIImageView *iconImageView;
|
||||||
|
@property (nonatomic, strong) UIView *separatorLine;
|
||||||
@property (nonatomic, weak) id<MenuSectionDelegate> delegate; // Delegate property
|
@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)addSubStackView:(UIStackView *)stackView;
|
||||||
- (void)addToParentStack:(UIStackView *)parentStack;
|
- (void)addToParentStack:(UIStackView *)parentStack;
|
||||||
- (void)removeSubStackView:(UIStackView *)stackView;
|
- (void)removeSubStackView:(UIStackView *)stackView;
|
||||||
- (void)updateLayout;
|
- (void)updateLayout;
|
||||||
- (void)updateViewForFoldState;
|
- (void)updateViewForFoldState;
|
||||||
|
- (void)setExpanded:(BOOL)overridePersistedState;
|
||||||
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -8,24 +8,32 @@
|
|||||||
// MenuSectionView.m
|
// MenuSectionView.m
|
||||||
|
|
||||||
#import "MenuSectionView.h"
|
#import "MenuSectionView.h"
|
||||||
#import "ThemeManager.h"
|
#import "VoidLink-Swift.h"
|
||||||
|
|
||||||
|
|
||||||
@interface MenuSectionView ()
|
@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 *toggleButton;
|
||||||
@property (nonatomic, strong) UIButton *toggleArea;
|
@property (nonatomic, strong) UIButton *toggleArea;
|
||||||
@property (nonatomic, strong) NSLayoutConstraint *heightConstraint;
|
@property (nonatomic, strong) NSLayoutConstraint *heightConstraint;
|
||||||
@property (nonatomic, strong) UIView *headerView;
|
@property (nonatomic, strong) UIView *headerView;
|
||||||
@property (nonatomic, strong) UIView *separatorLine;
|
@property (assign) CGFloat titleOffset;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation MenuSectionView
|
@implementation MenuSectionView
|
||||||
|
|
||||||
|
static BOOL overridePersistedFoldState = YES;
|
||||||
|
|
||||||
|
+ (BOOL)overridePersistedFoldState {
|
||||||
|
return overridePersistedFoldState;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (void)setOverridePersistedFoldState:(BOOL)val {
|
||||||
|
overridePersistedFoldState = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
- (instancetype)initWithFrame:(CGRect)frame {
|
- (instancetype)initWithFrame:(CGRect)frame {
|
||||||
self = [super initWithFrame:frame];
|
self = [super initWithFrame:frame];
|
||||||
if (self) {
|
if (self) {
|
||||||
@@ -44,15 +52,17 @@
|
|||||||
|
|
||||||
- (void)commonInit {
|
- (void)commonInit {
|
||||||
// 默认值
|
// 默认值
|
||||||
_leadingTrailingPadding = 0;
|
_leadingTrailingPadding = 5;
|
||||||
_separatorLinePadding = 40;
|
_separatorLinePadding = 40;
|
||||||
_sectionTitle = @"Section";
|
_sectionTitle = @"Section";
|
||||||
_expanded = YES;
|
_isExpanded = YES;
|
||||||
|
_expandable = true;
|
||||||
_backgroundColor = [UIColor clearColor];
|
_backgroundColor = [UIColor clearColor];
|
||||||
_rootStackViewSpacing = [self isIPhone] ? 10 : 18;
|
_rootStackViewSpacing = [self isIPhone] ? 10 : 12;
|
||||||
_subStackViews = [NSMutableArray array];
|
_subStackViews = [NSMutableArray array];
|
||||||
_headerViewHeight = 37;
|
_headerViewHeight = 37;
|
||||||
_headerViewVerticalSpacing = 25;
|
_headerViewVerticalSpacing = 25;
|
||||||
|
_titleOffset = 41.5;
|
||||||
|
|
||||||
// 设置视图
|
// 设置视图
|
||||||
self.layer.cornerRadius = 10.0;
|
self.layer.cornerRadius = 10.0;
|
||||||
@@ -68,14 +78,15 @@
|
|||||||
_iconImageView = [[UIImageView alloc] init];
|
_iconImageView = [[UIImageView alloc] init];
|
||||||
_iconImageView.contentMode = UIViewContentModeScaleAspectFit;
|
_iconImageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||||
_iconImageView.translatesAutoresizingMaskIntoConstraints = NO;
|
_iconImageView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
_iconImageView.tintColor = [ThemeManager textColor];
|
_iconImageView.tintColor = ThemeManager.textColor;
|
||||||
[_headerView addSubview:_iconImageView];
|
[_headerView addSubview:_iconImageView];
|
||||||
|
|
||||||
// 标题标签
|
// 标题标签
|
||||||
_titleLabel = [[UILabel alloc] init];
|
_titleLabel = [[UILabel alloc] init];
|
||||||
_titleLabel.text = _sectionTitle;
|
_titleLabel.text = _sectionTitle;
|
||||||
_titleLabel.font = [UIFont systemFontOfSize:23 weight:UIFontWeightMedium];
|
_titleLabel.accessibilityIdentifier = @"menuSectionTitleLabel";
|
||||||
_titleLabel.textColor = [ThemeManager textColor];
|
_titleLabel.font = [UIFont systemFontOfSize:19.5 weight:UIFontWeightMedium];
|
||||||
|
_titleLabel.textColor = ThemeManager.textColor;
|
||||||
_titleLabel.textAlignment = NSTextAlignmentLeft;
|
_titleLabel.textAlignment = NSTextAlignmentLeft;
|
||||||
_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
[_headerView addSubview:_titleLabel];
|
[_headerView addSubview:_titleLabel];
|
||||||
@@ -85,7 +96,7 @@
|
|||||||
_toggleArea = [UIButton buttonWithType:UIButtonTypeSystem];
|
_toggleArea = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||||
if (@available(iOS 13.0, *)) {
|
if (@available(iOS 13.0, *)) {
|
||||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:_headerViewHeight*0.31 weight:UIImageSymbolWeightBold];
|
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 {
|
} else {
|
||||||
// [_toggleButton setTitle:@"<" forState:UIControlStateNormal];
|
// [_toggleButton setTitle:@"<" forState:UIControlStateNormal];
|
||||||
}
|
}
|
||||||
@@ -107,7 +118,7 @@
|
|||||||
|
|
||||||
// 分隔线
|
// 分隔线
|
||||||
_separatorLine = [[UIView alloc] init];
|
_separatorLine = [[UIView alloc] init];
|
||||||
_separatorLine.backgroundColor = [ThemeManager separatorColor];
|
_separatorLine.backgroundColor = ThemeManager.separatorColor;
|
||||||
_separatorLine.translatesAutoresizingMaskIntoConstraints = NO;
|
_separatorLine.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
|
||||||
[self addSubview:_separatorLine];
|
[self addSubview:_separatorLine];
|
||||||
@@ -119,6 +130,10 @@
|
|||||||
// 布局
|
// 布局
|
||||||
[self setupConstraints];
|
[self setupConstraints];
|
||||||
[self updateViewForFoldState];
|
[self updateViewForFoldState];
|
||||||
|
|
||||||
|
self.lockedSectionHandler = ^{
|
||||||
|
NSLog(@"Null execution of lockedSectionHandler");
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)setupConstraints {
|
- (void)setupConstraints {
|
||||||
@@ -129,20 +144,11 @@
|
|||||||
[_headerView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
|
[_headerView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
|
||||||
[_headerView.heightAnchor constraintEqualToConstant:_headerViewHeight]
|
[_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, *)){
|
if(@available(iOS 13.0, *)){
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
[_titleLabel.leadingAnchor constraintEqualToAnchor:_iconImageView.trailingAnchor constant:8],
|
[_titleLabel.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:_titleOffset],
|
||||||
[_titleLabel.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
|
[_titleLabel.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
|
||||||
[_titleLabel.trailingAnchor constraintEqualToAnchor:_toggleButton.leadingAnchor constant:-8]
|
[_titleLabel.trailingAnchor constraintEqualToAnchor:_toggleButton.leadingAnchor constant:-8]
|
||||||
]];
|
]];
|
||||||
@@ -166,7 +172,7 @@
|
|||||||
[_toggleArea.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
|
[_toggleArea.centerYAnchor constraintEqualToAnchor:_headerView.centerYAnchor],
|
||||||
[_toggleArea.trailingAnchor constraintEqualToAnchor:_toggleButton.leadingAnchor],
|
[_toggleArea.trailingAnchor constraintEqualToAnchor:_toggleButton.leadingAnchor],
|
||||||
//[_toggleArea.widthAnchor constraintEqualToConstant:_headerViewHeight],
|
//[_toggleArea.widthAnchor constraintEqualToConstant:_headerViewHeight],
|
||||||
[_toggleArea.heightAnchor constraintEqualToConstant:_headerViewHeight]
|
[_toggleArea.heightAnchor constraintEqualToConstant:_headerViewHeight+25]
|
||||||
]];
|
]];
|
||||||
|
|
||||||
[self layoutIfNeeded];
|
[self layoutIfNeeded];
|
||||||
@@ -185,9 +191,9 @@
|
|||||||
//[_separatorLine.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
|
//[_separatorLine.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
|
||||||
//[_separatorLine.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
|
//[_separatorLine.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
|
||||||
[_separatorLine.centerXAnchor constraintEqualToAnchor:self.centerXAnchor],
|
[_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.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
|
||||||
[_separatorLine.heightAnchor constraintEqualToConstant:2]
|
[_separatorLine.heightAnchor constraintEqualToConstant:GenericUtils.menuSectionSeparatorWidth]
|
||||||
]];
|
]];
|
||||||
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
@@ -198,7 +204,7 @@
|
|||||||
|
|
||||||
#pragma mark - Public Methods
|
#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, *)) {
|
if (@available(iOS 13.0, *)) {
|
||||||
UIImageSymbolConfiguration *config;
|
UIImageSymbolConfiguration *config;
|
||||||
if(icon.isSymbolImage) config = [UIImageSymbolConfiguration configurationWithPointSize:size weight:UIImageSymbolWeightBold];
|
if(icon.isSymbolImage) config = [UIImageSymbolConfiguration configurationWithPointSize:size weight:UIImageSymbolWeightBold];
|
||||||
@@ -210,8 +216,44 @@
|
|||||||
} else {
|
} else {
|
||||||
_iconImageView.image = icon;
|
_iconImageView.image = icon;
|
||||||
}
|
}
|
||||||
_iconImageView.tintColor = [ThemeManager textColor];
|
_iconImageView.tintColor = ThemeManager.textColor;
|
||||||
_iconImageView.hidden = (icon == nil);
|
_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];
|
[self updateLayout];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,9 +268,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)setExpanded:(BOOL)isExpanded {
|
- (void)setExpanded:(BOOL)isExpanded {
|
||||||
if (_expanded != isExpanded) {
|
self.isExpanded = !_expandable ? false : isExpanded;
|
||||||
_expanded = isExpanded;}
|
[self updateViewForFoldState];
|
||||||
[self updateViewForFoldState];
|
[[NSUserDefaults standardUserDefaults] setBool:self.isExpanded forKey:self.identifier];
|
||||||
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||||
@@ -264,6 +307,11 @@
|
|||||||
[self.leadingAnchor constraintEqualToAnchor:parentStack.leadingAnchor constant:0],
|
[self.leadingAnchor constraintEqualToAnchor:parentStack.leadingAnchor constant:0],
|
||||||
[self.trailingAnchor constraintEqualToAnchor:parentStack.trailingAnchor 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 {
|
- (void)removeSubStackView:(UIStackView *)stackView {
|
||||||
@@ -308,8 +356,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)toggleFold {
|
- (void)toggleFold {
|
||||||
self.expanded = !self.expanded;
|
self.isExpanded = !_expandable ? false : !self.isExpanded;
|
||||||
|
if(!_expandable && self.lockedSectionHandler) self.lockedSectionHandler();
|
||||||
[self updateViewForFoldState];
|
[self updateViewForFoldState];
|
||||||
|
[[NSUserDefaults standardUserDefaults] setBool:self.isExpanded forKey:self.identifier];
|
||||||
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (SettingsMenuMode)getSettingsMenuMode {
|
- (SettingsMenuMode)getSettingsMenuMode {
|
||||||
@@ -320,7 +371,9 @@
|
|||||||
|
|
||||||
|
|
||||||
- (void)updateViewForFoldState {
|
- (void)updateViewForFoldState {
|
||||||
[self setupConstraints];
|
// [self setupConstraints];
|
||||||
|
// don't run this repeatedly
|
||||||
|
|
||||||
NSInteger visibleCount = 0;
|
NSInteger visibleCount = 0;
|
||||||
for (UIView *subview in _rootStackView.arrangedSubviews) {
|
for (UIView *subview in _rootStackView.arrangedSubviews) {
|
||||||
if (!subview.isHidden) {
|
if (!subview.isHidden) {
|
||||||
@@ -328,7 +381,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.hidden = visibleCount == 0;
|
self.hidden = visibleCount == 0;
|
||||||
if (_expanded) {
|
if (_isExpanded) {
|
||||||
_rootStackView.hidden = NO;
|
_rootStackView.hidden = NO;
|
||||||
_separatorLine.hidden = NO;
|
_separatorLine.hidden = NO;
|
||||||
CGSize fittingSize = [self.rootStackView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
|
CGSize fittingSize = [self.rootStackView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
|
||||||
@@ -340,7 +393,7 @@
|
|||||||
self.heightConstraint.constant = headerHeight + _headerViewVerticalSpacing + rootStackViewHeight + _separatorLinePadding + 1;
|
self.heightConstraint.constant = headerHeight + _headerViewVerticalSpacing + rootStackViewHeight + _separatorLinePadding + 1;
|
||||||
|
|
||||||
[UIView animateWithDuration:0 animations:^{
|
[UIView animateWithDuration:0 animations:^{
|
||||||
self.toggleButton.transform = CGAffineTransformMakeRotation(M_PI+M_PI_2);
|
self.toggleButton.transform = CGAffineTransformMakeRotation(M_PI/2);
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
[self.rootStackView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-self.separatorLinePadding],
|
[self.rootStackView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-self.separatorLinePadding],
|
||||||
// [self.rootStackView.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor constant:0],
|
// [self.rootStackView.topAnchor constraintEqualToAnchor:self.headerView.bottomAnchor constant:0],
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#import "ConnectionCallbacks.h"
|
#import "ConnectionCallbacks.h"
|
||||||
#import "Frame.h"
|
#import "Frame.h"
|
||||||
#import "Plot.h"
|
#import "Plot.h"
|
||||||
|
#import "TemporarySettings.h"
|
||||||
|
|
||||||
@interface MetalVideoRenderer : NSObject
|
@interface MetalVideoRenderer : NSObject
|
||||||
|
|
||||||
@@ -22,11 +23,13 @@
|
|||||||
@property (nonatomic) id<CAMetalDrawable> _Nullable nextDrawable;
|
@property (nonatomic) id<CAMetalDrawable> _Nullable nextDrawable;
|
||||||
@property (atomic) BOOL isStopping;
|
@property (atomic) BOOL isStopping;
|
||||||
@property (nonatomic) BOOL hdrEnabled;
|
@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;
|
- (instancetype _Nonnull )initWithMetalDevice:(id<MTLDevice>_Nonnull)device drawablePixelFormat:(MTLPixelFormat)drawablePixelFormat settings:(TemporarySettings* _Nonnull )currentSettings;
|
||||||
- (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;
|
- (void)renderFrame:(nonnull Frame *)frame toLayer:(nonnull CAMetalLayer *)layer;
|
||||||
|
- (BOOL)waitToRenderTo:(nonnull CAMetalLayer *)layer API_AVAILABLE(ios(13.0));
|
||||||
- (void)drawableResize:(CGSize)drawableSize;
|
- (void)drawableResize:(CGSize)drawableSize;
|
||||||
- (void)shutdown;
|
- (void)shutdown;
|
||||||
|
|
||||||
|
|||||||
@@ -130,25 +130,36 @@ CFStringRef __currentColorSpace;
|
|||||||
size_t _lastDrawableHeight;
|
size_t _lastDrawableHeight;
|
||||||
id<MTLBuffer> _CscParamsBuffer;
|
id<MTLBuffer> _CscParamsBuffer;
|
||||||
id<MTLBuffer> _VideoVertexBuffer;
|
id<MTLBuffer> _VideoVertexBuffer;
|
||||||
|
|
||||||
|
CFStringRef _nonFullHdrColorSpace;
|
||||||
|
MTLPixelFormat _nonFullHdrPixelFormat;
|
||||||
|
|
||||||
// https://developer.apple.com/documentation/metal/synchronizing-cpu-and-gpu-work?language=objc
|
// https://developer.apple.com/documentation/metal/synchronizing-cpu-and-gpu-work?language=objc
|
||||||
dispatch_semaphore_t _inFlightSemaphore;
|
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];
|
self = [super init];
|
||||||
if (self) {
|
if (self) {
|
||||||
_sq = dispatch_queue_create("com.moonlight.MetalVideoRenderer",
|
_sq = dispatch_queue_create("com.moonlight.MetalVideoRenderer",
|
||||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0));
|
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;
|
_device = device;
|
||||||
_colorPixelFormat = MTLPixelFormatBGR10A2Unorm;
|
_colorPixelFormat = drawablePixelFormat;
|
||||||
_framerate = framerate;
|
_framerate = currentSettings.framerate.floatValue;
|
||||||
_hdrEnabled = hdrEnabled;
|
_hdrEnabled = currentSettings.enableHdr;
|
||||||
_commandQueue = [_device newCommandQueue];
|
_commandQueue = [_device newCommandQueue];
|
||||||
_currentEDRHeadroom = 1.0f;
|
_currentEDRHeadroom = 1.0f;
|
||||||
_lastColorSpace = -1;
|
_lastColorSpace = -1;
|
||||||
_lastFullRange = NO;
|
_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;
|
_lastPresented = 0.0f;
|
||||||
_inFlightSemaphore = dispatch_semaphore_create(MaxFramesInFlight);
|
_inFlightSemaphore = dispatch_semaphore_create(MaxFramesInFlight);
|
||||||
_isStopping = NO;
|
_isStopping = NO;
|
||||||
@@ -164,6 +175,10 @@ CFStringRef __currentColorSpace;
|
|||||||
_renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0);
|
_renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0);
|
||||||
_renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
|
_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
|
// Initialize texture array to NULL
|
||||||
for (int i = 0; i < MAX_VIDEO_PLANES; i++) {
|
for (int i = 0; i < MAX_VIDEO_PLANES; i++) {
|
||||||
_cvMetalTextures[i] = NULL;
|
_cvMetalTextures[i] = NULL;
|
||||||
@@ -393,16 +408,12 @@ CFStringRef __currentColorSpace;
|
|||||||
CFStringRef frame_trc = CFDictionaryGetValue(ext, kCVImageBufferTransferFunctionKey);
|
CFStringRef frame_trc = CFDictionaryGetValue(ext, kCVImageBufferTransferFunctionKey);
|
||||||
if (CFEqual(frame_trc, kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ)) {
|
if (CFEqual(frame_trc, kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ)) {
|
||||||
isHDR = YES;
|
isHDR = YES;
|
||||||
if (@available(iOS 14.0, *)) {
|
newColorSpace = CGColorSpaceCreateWithName(_nonFullHdrColorSpace);
|
||||||
newColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_2100_PQ);
|
newPixelFormat = _nonFullHdrPixelFormat;
|
||||||
} else {
|
|
||||||
newColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_2020);
|
|
||||||
}
|
|
||||||
newPixelFormat = MTLPixelFormatBGR10A2Unorm;
|
|
||||||
} else {
|
} else {
|
||||||
// SDR 2020, I'm not sure it's possible to stream this though
|
// SDR 2020, I'm not sure it's possible to stream this though
|
||||||
newColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_2020);
|
newColorSpace = CGColorSpaceCreateWithName(_nonFullHdrColorSpace);
|
||||||
newPixelFormat = MTLPixelFormatBGR10A2Unorm;
|
newPixelFormat = _nonFullHdrPixelFormat;
|
||||||
}
|
}
|
||||||
if (isHDR) {
|
if (isHDR) {
|
||||||
paramBuffer.cscParams = (fullRange ? k_CscParams_Bt2020Full_10bit : k_CscParams_Bt2020Lim_10bit);
|
paramBuffer.cscParams = (fullRange ? k_CscParams_Bt2020Full_10bit : k_CscParams_Bt2020Lim_10bit);
|
||||||
@@ -433,11 +444,11 @@ CFStringRef __currentColorSpace;
|
|||||||
Log(LOG_I,
|
Log(LOG_I,
|
||||||
@"Frame pixel format %@ - changing MetalLayer's pixel format to %@",
|
@"Frame pixel format %@ - changing MetalLayer's pixel format to %@",
|
||||||
layer.pixelFormat == MTLPixelFormatBGRA8Unorm ? @"MTLPixelFormatBGRA8Unorm"
|
layer.pixelFormat == MTLPixelFormatBGRA8Unorm ? @"MTLPixelFormatBGRA8Unorm"
|
||||||
: layer.pixelFormat == MTLPixelFormatBGR10A2Unorm ? @"MTLPixelFormatBGR10A2Unorm"
|
: layer.pixelFormat == MTLPixelFormatBGRA10_XR ? @"MTLPixelFormatBGRA10_XR"
|
||||||
: [NSString stringWithFormat:@"Unknown: %lu", layer.pixelFormat],
|
: [NSString stringWithFormat:@"Unknown: %lu", layer.pixelFormat],
|
||||||
newPixelFormat == MTLPixelFormatBGRA8Unorm ? @"MTLPixelFormatBGRA8Unorm"
|
newPixelFormat == MTLPixelFormatBGRA8Unorm ? @"MTLPixelFormatBGRA8Unorm"
|
||||||
: newPixelFormat == MTLPixelFormatBGR10A2Unorm ? @"MTLPixelFormatBGR10A2Unorm"
|
: newPixelFormat == MTLPixelFormatBGRA10_XR ? @"MTLPixelFormatBGRA10_XR"
|
||||||
: [NSString stringWithFormat:@"Unknown: %lu", (unsigned long)layer.pixelFormat]);
|
: [NSString stringWithFormat:@"Unknown: %lu", (unsigned long)newPixelFormat]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
#if TARGET_OS_TV
|
||||||
@@ -488,6 +499,10 @@ CFStringRef __currentColorSpace;
|
|||||||
_lastColorSpace = colorspace;
|
_lastColorSpace = colorspace;
|
||||||
_lastFullRange = fullRange;
|
_lastFullRange = fullRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// NSLog(@"layer.pixelFormat %lu", (unsigned long)layer.pixelFormat);
|
||||||
|
|
||||||
|
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
@@ -557,29 +572,21 @@ CFStringRef __currentColorSpace;
|
|||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)renderFrame:(Frame *)frame toLayer:(CAMetalLayer *)layer {
|
||||||
- (void)renderFrame:(Frame *)frame withDrawable:(id<CAMetalDrawable>)drawable targetPresentationTimestamp:(CFTimeInterval)targetPresentationTimestamp API_AVAILABLE(ios(17.0)) {
|
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
if (self.isStopping) {
|
if (self.isStopping) {
|
||||||
Log(LOG_I, @"[MetalVideoRenderer] isStopping");
|
Log(LOG_I, @"[MetalVideoRenderer] isStopping");
|
||||||
return;
|
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
|
// Handle changes to the frame's colorspace from last time we rendered
|
||||||
BOOL layerDidChange = NO;
|
BOOL layerDidChange = NO;
|
||||||
if (![self updateColorSpaceForFrame:frame toLayer:(CAMetalLayer *)drawable.layer layerDidChange:&layerDidChange]) {
|
if (![self updateColorSpaceForFrame:frame toLayer:layer layerDidChange:&layerDidChange]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle changes to the video size or drawable size
|
// Handle changes to the video size or drawable size
|
||||||
if (![self updateVideoRegionSizeForFrame:frame toLayer:(CAMetalLayer *)drawable.layer]) {
|
if (![self updateVideoRegionSizeForFrame:frame toLayer:layer]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,6 +598,15 @@ CFStringRef __currentColorSpace;
|
|||||||
}
|
}
|
||||||
|
|
||||||
size_t planes = CVPixelBufferGetPlaneCount(frame.pixelBuffer);
|
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);
|
assert(planes <= MAX_VIDEO_PLANES);
|
||||||
|
|
||||||
if (layerDidChange && frame.frameNumber > 1) {
|
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
|
// Get the framebuffer pixel format for pipeline creation
|
||||||
MTLPixelFormat framebufferPixelFormat = drawable.texture.pixelFormat;
|
MTLPixelFormat framebufferPixelFormat = drawable.texture.pixelFormat;
|
||||||
|
|
||||||
@@ -622,16 +645,26 @@ CFStringRef __currentColorSpace;
|
|||||||
// linear shaders
|
// linear shaders
|
||||||
id<MTLFunction> yuvToLinear = [defaultLibrary newFunctionWithName:@"yuvToLinear"];
|
id<MTLFunction> yuvToLinear = [defaultLibrary newFunctionWithName:@"yuvToLinear"];
|
||||||
|
|
||||||
// Determine if this is 10-bit based on the framebuffer's pixel format
|
// Determine if this is 10-bit based on the input CVPixelBuffer format, not the output framebuffer format
|
||||||
BOOL is10Bit = (framebufferPixelFormat == MTLPixelFormatBGR10A2Unorm);
|
// pixelFormatType is already declared above
|
||||||
|
BOOL is10BitInput = (pixelFormatType == kCVPixelFormatType_420YpCbCr10BiPlanarFullRange ||
|
||||||
|
pixelFormatType == kCVPixelFormatType_444YpCbCr10BiPlanarFullRange ||
|
||||||
|
pixelFormatType == kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange ||
|
||||||
|
pixelFormatType == kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange);
|
||||||
|
|
||||||
NSString *fragmentShaderName;
|
NSString *fragmentShaderName;
|
||||||
if (planes == 2) {
|
if (isPackedFormat) {
|
||||||
fragmentShaderName = is10Bit ? @"ps_draw_biplanar_10bit" : @"ps_draw_biplanar_8bit";
|
// 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 {
|
} 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.colorAttachments[0].pixelFormat = framebufferPixelFormat;
|
||||||
pipelineDesc.vertexBuffers[0].mutability = MTLMutabilityImmutable;
|
pipelineDesc.vertexBuffers[0].mutability = MTLMutabilityImmutable;
|
||||||
|
|
||||||
@@ -659,41 +692,78 @@ CFStringRef __currentColorSpace;
|
|||||||
_videoPipelinePixelFormat[planes] = framebufferPixelFormat;
|
_videoPipelinePixelFormat[planes] = framebufferPixelFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (size_t i = 0; i < planes; i++) {
|
if (isPackedFormat) {
|
||||||
MTLPixelFormat fmt;
|
// Handle packed BGRA format - iOS/macOS uses BGRA internally
|
||||||
|
// Check if the pixel buffer has IOSurface backing
|
||||||
switch (CVPixelBufferGetPixelFormatType(frame.pixelBuffer)) {
|
CFTypeRef ioSurface = CVPixelBufferGetIOSurface(frame.pixelBuffer);
|
||||||
case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
|
if (!ioSurface) {
|
||||||
case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
|
Log(LOG_E, @"CVPixelBuffer does not have IOSurface backing - cannot create Metal texture");
|
||||||
case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
|
return;
|
||||||
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,
|
CVReturn err = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
|
||||||
_textureCache,
|
_textureCache,
|
||||||
frame.pixelBuffer,
|
frame.pixelBuffer,
|
||||||
NULL,
|
NULL,
|
||||||
fmt,
|
MTLPixelFormatBGRA8Unorm,
|
||||||
CVPixelBufferGetWidthOfPlane(frame.pixelBuffer, i),
|
CVPixelBufferGetWidth(frame.pixelBuffer),
|
||||||
CVPixelBufferGetHeightOfPlane(frame.pixelBuffer, i),
|
CVPixelBufferGetHeight(frame.pixelBuffer),
|
||||||
i,
|
0, // planeIndex must be 0 for non-planar
|
||||||
&_cvMetalTextures[i]);
|
&_cvMetalTextures[0]);
|
||||||
if (err != kCVReturnSuccess) {
|
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;
|
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];
|
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:_renderPassDescriptor];
|
||||||
|
|
||||||
[renderEncoder setRenderPipelineState:_videoPipelineState[planes]];
|
[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 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 !TARGET_OS_TV
|
||||||
if (((CAMetalLayer *)drawable.layer).pixelFormat == MTLPixelFormatRGBA16Float) {
|
if (layer.pixelFormat == MTLPixelFormatRGBA16Float) {
|
||||||
[self pollCurrentEDRHeadroom];
|
[self pollCurrentEDRHeadroom];
|
||||||
[renderEncoder setFragmentBytes:&_currentEDRHeadroom length:sizeof(float) atIndex:1];
|
[renderEncoder setFragmentBytes:&_currentEDRHeadroom length:sizeof(float) atIndex:1];
|
||||||
}
|
}
|
||||||
@@ -718,33 +800,29 @@ CFStringRef __currentColorSpace;
|
|||||||
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
|
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
|
||||||
[renderEncoder endEncoding];
|
[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
|
// signal semaphore, compute GPU time average, and clear textures
|
||||||
__block dispatch_semaphore_t block_semaphore = _inFlightSemaphore;
|
__block dispatch_semaphore_t block_semaphore = _inFlightSemaphore;
|
||||||
|
__block size_t texturesToClean = isPackedFormat ? 1 : CVPixelBufferGetPlaneCount(frame.pixelBuffer);
|
||||||
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> cb) {
|
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> cb) {
|
||||||
dispatch_semaphore_signal(block_semaphore);
|
dispatch_semaphore_signal(block_semaphore);
|
||||||
|
|
||||||
if (cb.error) {
|
const CFTimeInterval GPUTime = cb.GPUEndTime - cb.GPUStartTime;
|
||||||
Log(LOG_E, @"Command buffer error: %@", cb.error);
|
const double alpha = 0.25f;
|
||||||
// Request IDR frame to recover from rendering error
|
self->_averageGPUTime = (GPUTime * alpha) + (self->_averageGPUTime * (1.0 - alpha));
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Free textures after completion of rendering
|
// 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]) {
|
if (self->_cvMetalTextures[i]) {
|
||||||
CFRelease(self->_cvMetalTextures[i]);
|
CFRelease(self->_cvMetalTextures[i]);
|
||||||
self->_cvMetalTextures[i] = NULL;
|
self->_cvMetalTextures[i] = NULL;
|
||||||
@@ -754,27 +832,32 @@ CFStringRef __currentColorSpace;
|
|||||||
CVMetalTextureCacheFlush(self->_textureCache, 0);
|
CVMetalTextureCacheFlush(self->_textureCache, 0);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// CAMetalDisplayLink handles timing automatically, so we don't use afterMinimumDuration
|
#if TARGET_OS_SIMULATOR
|
||||||
[commandBuffer presentDrawable:drawable];
|
[commandBuffer presentDrawable:drawable];
|
||||||
|
#else
|
||||||
|
// present for a minimum duration for best frame pacing
|
||||||
|
[commandBuffer presentDrawable:drawable afterMinimumDuration:1.0f / _framerate];
|
||||||
|
#endif
|
||||||
|
|
||||||
[commandBuffer commit];
|
[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
|
// Wait to ensure only `MaxFramesInFlight` number of frames are getting processed
|
||||||
// by any stage in the Metal pipeline (CPU, GPU, Metal, Drivers, etc.).
|
// by any stage in the Metal pipeline (CPU, GPU, Metal, Drivers, etc.).
|
||||||
if (!self.isStopping) {
|
if (self.isStopping) {
|
||||||
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)); // 100ms
|
return NO;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
- (void)shutdown {
|
||||||
|
|||||||
@@ -15,14 +15,13 @@
|
|||||||
@protocol MetalViewDelegate <NSObject>
|
@protocol MetalViewDelegate <NSObject>
|
||||||
|
|
||||||
- (void)drawableResize:(CGSize)size;
|
- (void)drawableResize:(CGSize)size;
|
||||||
- (void)renderWithDrawable:(nonnull id<CAMetalDrawable>)drawable toLayer:(nonnull CAMetalLayer *)layer API_AVAILABLE(ios(17.0));
|
- (void)renderTo:(nonnull CAMetalLayer *)layer;
|
||||||
- (void)renderWithDrawable:(nonnull id<CAMetalDrawable>)drawable toLayer:(nonnull CAMetalLayer *)layer targetPresentationTimestamp:(CFTimeInterval)targetPresentationTimestamp API_AVAILABLE(ios(17.0));
|
|
||||||
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
|
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
// The Metal game view base class.
|
// The Metal game view base class.
|
||||||
@interface MetalView : UIView <CALayerDelegate, CAMetalDisplayLinkDelegate>
|
@interface MetalView : UIView <CALayerDelegate>
|
||||||
|
|
||||||
@property (nonatomic, nonnull, readonly) CAMetalLayer *metalLayer;
|
@property (nonatomic, nonnull, readonly) CAMetalLayer *metalLayer;
|
||||||
@property (nonatomic, nullable) id<MetalViewDelegate> delegate;
|
@property (nonatomic, nullable) id<MetalViewDelegate> delegate;
|
||||||
|
|||||||
@@ -11,12 +11,10 @@
|
|||||||
|
|
||||||
#import "MetalView.h"
|
#import "MetalView.h"
|
||||||
#import "MetalConfig.h"
|
#import "MetalConfig.h"
|
||||||
#import <QuartzCore/CAMetalDisplayLink.h>
|
|
||||||
|
|
||||||
@implementation MetalView {
|
@implementation MetalView {
|
||||||
// The secondary thread containing the render loop.
|
// The secondary thread containing the render loop.
|
||||||
NSThread *_renderThread;
|
NSThread *_renderThread;
|
||||||
CAMetalDisplayLink *_metalDisplayLink API_AVAILABLE(ios(17.0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Initialization and Setup.
|
#pragma mark - Initialization and Setup.
|
||||||
@@ -42,43 +40,15 @@
|
|||||||
self.layer.delegate = self;
|
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 {
|
- (void)shutdown {
|
||||||
// First cancel the thread to stop the run loop
|
|
||||||
if (_renderThread) {
|
if (_renderThread) {
|
||||||
Log(LOG_I, @"[MetalView] sending renderThread a cancel message");
|
Log(LOG_I, @"[MetalView] sending renderThread a cancel message");
|
||||||
[_renderThread cancel];
|
[_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");
|
Log(LOG_I, @"[MetalView] waiting on renderThread to finish");
|
||||||
NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:2.0]; // 2 second timeout
|
while (!_renderThread.isFinished) {
|
||||||
while (!_renderThread.isFinished && [timeout timeIntervalSinceNow] > 0) {
|
usleep(100);
|
||||||
usleep(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_renderThread.isFinished) {
|
|
||||||
Log(LOG_I, @"[MetalView] renderThread has finished");
|
|
||||||
} else {
|
|
||||||
Log(LOG_E, @"[MetalView] renderThread failed to finish in time");
|
|
||||||
}
|
}
|
||||||
|
Log(LOG_I, @"[MetalView] renderThread has finished");
|
||||||
_renderThread = nil;
|
_renderThread = nil;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,26 +61,6 @@
|
|||||||
[self movedToWindow];
|
[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 {
|
- (void)movedToWindow {
|
||||||
if (!self.window) {
|
if (!self.window) {
|
||||||
Log(LOG_I, @"[MetalView] movedToWindow(nil): shutting down...");
|
Log(LOG_I, @"[MetalView] movedToWindow(nil): shutting down...");
|
||||||
@@ -118,32 +68,12 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (@available(iOS 17.0, *)) {
|
// Render on a new thread
|
||||||
_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
|
|
||||||
_renderThread = [[NSThread alloc] initWithBlock:^{
|
_renderThread = [[NSThread alloc] initWithBlock:^{
|
||||||
// Add metal display link to this thread's run loop
|
while (![NSThread currentThread].isCancelled) {
|
||||||
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) {
|
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
// Run the run loop for a short time to allow checking cancellation
|
[self.delegate waitToRenderTo:self.metalLayer];
|
||||||
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
|
[self.delegate renderTo:self.metalLayer];
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from run loop if still valid
|
|
||||||
if (@available(iOS 17.0, *)) {
|
|
||||||
if (self->_metalDisplayLink) {
|
|
||||||
[self->_metalDisplayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log(LOG_I, @"[MetalView] renderThread is exiting");
|
Log(LOG_I, @"[MetalView] renderThread is exiting");
|
||||||
@@ -151,7 +81,7 @@
|
|||||||
_renderThread.name = @"MetalVideoRenderer";
|
_renderThread.name = @"MetalVideoRenderer";
|
||||||
_renderThread.qualityOfService = NSQualityOfServiceUserInteractive;
|
_renderThread.qualityOfService = NSQualityOfServiceUserInteractive;
|
||||||
[_renderThread start];
|
[_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
|
// 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
|
// didMoveToWindow after the view initialization, this is the first opportunity to notify
|
||||||
|
|||||||
@@ -12,15 +12,14 @@
|
|||||||
#import "ImGuiRenderer.h"
|
#import "ImGuiRenderer.h"
|
||||||
#import "MetalVideoRenderer.h"
|
#import "MetalVideoRenderer.h"
|
||||||
#import "MetalView.h"
|
#import "MetalView.h"
|
||||||
|
#import "TemporarySettings.h"
|
||||||
|
|
||||||
@interface MetalViewController : UIViewController <MetalViewDelegate>
|
@interface MetalViewController : UIViewController <MetalViewDelegate>
|
||||||
|
|
||||||
@property (nonatomic) CGRect bounds;
|
@property (nonatomic) CGRect bounds;
|
||||||
|
|
||||||
- (nonnull instancetype)initWithFrame:(CGRect)bounds
|
- (nonnull instancetype)initWithFrame:(CGRect)bounds framerate:(float)framerate settings:(TemporarySettings* _Nonnull )settings metricsHandler:(MetricsHandler _Nonnull)metricsHandler;
|
||||||
framerate:(float)framerate
|
|
||||||
enableHdr:(BOOL)enableHdr
|
|
||||||
metricsHandler:(MetricsHandler _Nonnull)metricsHandler;
|
|
||||||
|
|
||||||
- (void)pauseRendering;
|
- (void)pauseRendering;
|
||||||
- (void)resumeRendering;
|
- (void)resumeRendering;
|
||||||
|
|||||||
@@ -14,19 +14,21 @@
|
|||||||
@implementation MetalViewController {
|
@implementation MetalViewController {
|
||||||
FrameQueue *_frameQueue;
|
FrameQueue *_frameQueue;
|
||||||
float _framerate;
|
float _framerate;
|
||||||
BOOL _enableHdr;
|
TemporarySettings* _currentSettings;
|
||||||
MetalView *_metalView;
|
MetalView *_metalView;
|
||||||
MetalVideoRenderer *_renderer;
|
MetalVideoRenderer *_renderer;
|
||||||
MetricsHandler _metricsHandler;
|
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];
|
self = [super init];
|
||||||
if (self) {
|
if (self) {
|
||||||
_bounds = bounds;
|
_bounds = bounds;
|
||||||
_frameQueue = [FrameQueue sharedInstance];
|
_frameQueue = [FrameQueue sharedInstance];
|
||||||
_framerate = framerate;
|
_framerate = framerate;
|
||||||
_enableHdr = enableHdr;
|
_currentSettings = settings;
|
||||||
_metricsHandler = metricsHandler;
|
_metricsHandler = metricsHandler;
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
@@ -58,11 +60,28 @@
|
|||||||
}
|
}
|
||||||
view.metalLayer.device = device;
|
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.
|
// Initialize the renderer.
|
||||||
MetalVideoRenderer *renderer = [[MetalVideoRenderer alloc] initWithMetalDevice:device
|
MetalVideoRenderer *renderer = [[MetalVideoRenderer alloc] initWithMetalDevice:device
|
||||||
drawablePixelFormat:MTLPixelFormatBGR10A2Unorm
|
drawablePixelFormat:pixelFormat
|
||||||
framerate:self->_framerate
|
settings:_currentSettings];
|
||||||
hdrEnabled:self->_enableHdr];
|
|
||||||
if (!renderer) {
|
if (!renderer) {
|
||||||
Log(LOG_E, @"The renderer couldn't be initialized.");
|
Log(LOG_E, @"The renderer couldn't be initialized.");
|
||||||
return;
|
return;
|
||||||
@@ -73,6 +92,20 @@
|
|||||||
// Initialize the renderer-dependent view properties.
|
// Initialize the renderer-dependent view properties.
|
||||||
view.metalLayer.pixelFormat = renderer.colorPixelFormat;
|
view.metalLayer.pixelFormat = renderer.colorPixelFormat;
|
||||||
view.metalLayer.maximumDrawableCount = 3;
|
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 {
|
- (void)waitToRenderTo:(nonnull CAMetalLayer *)layer {
|
||||||
@@ -81,36 +114,36 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renderer obtains a nextDrawable, waiting if necessary
|
|
||||||
if (@available(iOS 13.0, *)) {
|
if (@available(iOS 13.0, *)) {
|
||||||
[_renderer waitToRenderTo:layer];
|
_frameSlotAcquired = [_renderer waitToRenderTo:layer];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't have a frame yet, wait on that too
|
if (_frameSlotAcquired) {
|
||||||
[_frameQueue waitForEnqueue];
|
[_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;
|
CFTimeInterval timeout = (1.0f / _framerate) - _renderer.averageGPUTime;
|
||||||
Frame *frame = [_frameQueue dequeueWithTimeout:timeout];
|
Frame *frame = [_frameQueue dequeueWithTimeoutSync:timeout];
|
||||||
|
|
||||||
if (!_renderer.isStopping) {
|
if (!_renderer.isStopping) {
|
||||||
// Only render if not paused
|
// Only render if not paused
|
||||||
if (frame) {
|
if (frame) {
|
||||||
[_renderer renderFrame:frame withDrawable:drawable];
|
//if (@available(iOS 13.0, *)) {
|
||||||
}
|
[_renderer renderFrame:frame toLayer:layer];
|
||||||
}
|
//}
|
||||||
}
|
} else {
|
||||||
|
dispatch_semaphore_signal([_renderer inFlightSemaphore]);
|
||||||
- (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];
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// When paused, we still dequeue frames to prevent accumulation
|
// When paused, we still dequeue frames to prevent accumulation
|
||||||
// but don't render them. Also sleep a bit to reduce CPU usage
|
// but don't render them. Also sleep a bit to reduce CPU usage
|
||||||
|
dispatch_semaphore_signal([_renderer inFlightSemaphore]);
|
||||||
usleep(100000);
|
usleep(100000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,6 +153,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)pauseRendering {
|
- (void)pauseRendering {
|
||||||
|
if (_displayLink) {
|
||||||
|
_displayLink.paused = YES;
|
||||||
|
}
|
||||||
if (_renderer) {
|
if (_renderer) {
|
||||||
_renderer.isStopping = YES;
|
_renderer.isStopping = YES;
|
||||||
}
|
}
|
||||||
@@ -130,6 +166,9 @@
|
|||||||
if (_renderer) {
|
if (_renderer) {
|
||||||
_renderer.isStopping = NO;
|
_renderer.isStopping = NO;
|
||||||
}
|
}
|
||||||
|
if (_displayLink) {
|
||||||
|
_displayLink.paused = NO;
|
||||||
|
}
|
||||||
Log(LOG_I, @"[MetalViewController] Rendering resumed");
|
Log(LOG_I, @"[MetalViewController] Rendering resumed");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,13 +177,16 @@
|
|||||||
|
|
||||||
Log(LOG_I, @"[MetalViewController] viewDidDisappear");
|
Log(LOG_I, @"[MetalViewController] viewDidDisappear");
|
||||||
|
|
||||||
// Shutdown the renderer first
|
if (_displayLink) {
|
||||||
|
[_displayLink invalidate];
|
||||||
|
_displayLink = nil;
|
||||||
|
}
|
||||||
|
|
||||||
if (_renderer) {
|
if (_renderer) {
|
||||||
[_renderer shutdown];
|
[_renderer shutdown];
|
||||||
_renderer = nil;
|
_renderer = nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then shutdown the view's display link
|
|
||||||
if (_metalView) {
|
if (_metalView) {
|
||||||
_metalView.delegate = nil;
|
_metalView.delegate = nil;
|
||||||
[_metalView shutdown];
|
[_metalView shutdown];
|
||||||
|
|||||||
@@ -127,3 +127,11 @@ fragment float4 ps_draw_triplanar_10bit(Vertex v [[ stage_in ]],
|
|||||||
rgb.b = dot(yuv_corrected, cscParams.matrix[2]);
|
rgb.b = dot(yuv_corrected, cscParams.matrix[2]);
|
||||||
return float4(rgb, 1.0f);
|
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;
|
NSString* _uniqueId;
|
||||||
NSData* _cert;
|
NSData* _cert;
|
||||||
BOOL shouldDiscover;
|
BOOL shouldDiscover;
|
||||||
|
DataManager* dataMan;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id)initWithHosts:(NSArray *)hosts andCallback:(id<DiscoveryCallback>)callback {
|
- (id)initWithHosts:(NSArray *)hosts andCallback:(id<DiscoveryCallback>)callback {
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
[CryptoManager generateKeyPairUsingSSL];
|
[CryptoManager generateKeyPairUsingSSL];
|
||||||
_uniqueId = [IdManager getUniqueId];
|
_uniqueId = [IdManager getUniqueId];
|
||||||
_cert = [CryptoManager readCertFromFile];
|
_cert = [CryptoManager readCertFromFile];
|
||||||
|
DataManager* dataMan = [[DataManager alloc] init];
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +141,8 @@
|
|||||||
if ([serverInfoResponse isStatusOk]) {
|
if ([serverInfoResponse isStatusOk]) {
|
||||||
host = [[TemporaryHost alloc] init];
|
host = [[TemporaryHost alloc] init];
|
||||||
host.activeAddress = host.address = hostAddress;
|
host.activeAddress = host.address = hostAddress;
|
||||||
|
if(!host.activeAddressPool) host.activeAddressPool = [[NSMutableSet alloc] init];
|
||||||
|
[host.activeAddressPool addObject:host.activeAddress];
|
||||||
host.state = StateOnline;
|
host.state = StateOnline;
|
||||||
[serverInfoResponse populateHost:host];
|
[serverInfoResponse populateHost:host];
|
||||||
|
|
||||||
@@ -284,8 +288,9 @@
|
|||||||
existingHost.externalAddress = host.externalAddress;
|
existingHost.externalAddress = host.externalAddress;
|
||||||
}
|
}
|
||||||
existingHost.activeAddress = host.activeAddress;
|
existingHost.activeAddress = host.activeAddress;
|
||||||
|
[existingHost.activeAddressPool addObject:host.activeAddress];
|
||||||
existingHost.state = host.state;
|
existingHost.state = host.state;
|
||||||
return NO;
|
return NO; // returning a "NO" here means Updating addresses of existing host
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@synchronized (_hostQueue) {
|
@synchronized (_hostQueue) {
|
||||||
|
|||||||
@@ -41,11 +41,22 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (NSArray*) getHostAddressList {
|
- (NSArray*) getHostAddressList {
|
||||||
NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:3];
|
NSMutableArray *array = [[NSMutableArray alloc] init];
|
||||||
|
|
||||||
if (_host.localAddress != nil) {
|
if (_host.localAddress != nil) {
|
||||||
[array addObject:_host.localAddress];
|
[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) {
|
if (_host.address != nil) {
|
||||||
[array addObject:_host.address];
|
[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.
|
// to preserve insertion order of addresses.
|
||||||
for (int i = 0; i < [array count]; i++) {
|
for (int i = 0; i < [array count]; i++) {
|
||||||
NSString *addr1 = [array objectAtIndex:i];
|
NSString *addr1 = [array objectAtIndex:i];
|
||||||
|
// NSLog(@"iterates discovery addrs %@, host: %@", addr1, _host.name);
|
||||||
|
|
||||||
for (int j = 1; j < [array count]; j++) {
|
for (int j = 1; j < [array count]; j++) {
|
||||||
if (i == j) {
|
if (i == j) {
|
||||||
@@ -104,6 +116,8 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
|||||||
receivedResponse = [self checkResponse:serverInfoResp];
|
receivedResponse = [self checkResponse:serverInfoResp];
|
||||||
if (receivedResponse) {
|
if (receivedResponse) {
|
||||||
_host.activeAddress = address;
|
_host.activeAddress = address;
|
||||||
|
if(!_host.activeAddressPool) _host.activeAddressPool = [[NSMutableSet alloc] init];
|
||||||
|
[_host.activeAddressPool addObject:address];
|
||||||
[serverInfoResp populateHost:_host];
|
[serverInfoResp populateHost:_host];
|
||||||
|
|
||||||
// Update the database using the response
|
// Update the database using the response
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
- (NSURLRequest*) newLaunchOrResumeRequest:(NSString*)verb config:(StreamConfiguration*)config;
|
- (NSURLRequest*) newLaunchOrResumeRequest:(NSString*)verb config:(StreamConfiguration*)config;
|
||||||
- (NSURLRequest*) newQuitAppRequest;
|
- (NSURLRequest*) newQuitAppRequest;
|
||||||
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString*)appId;
|
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString*)appId;
|
||||||
|
- (NSURLRequest*) newBirateRequest:(NSInteger)bitrateKbps forClient:(NSString* )clientName;
|
||||||
- (void) executeRequestSynchronously:(HttpRequest*)request;
|
- (void) executeRequestSynchronously:(HttpRequest*)request;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -231,6 +231,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSString* urlString = [NSString stringWithFormat:@"%@/applist?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
NSString* urlString = [NSString stringWithFormat:@"%@/applist?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
||||||
|
NSLog(@"urlString: %@", urlString);
|
||||||
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,9 +293,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
NSString* urlString = [NSString stringWithFormat:@"%@/cancel?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
NSString* urlString = [NSString stringWithFormat:@"%@/cancel?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
||||||
|
NSLog(@"urlString print: %@", urlString);
|
||||||
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
|
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 {
|
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString *)appId {
|
||||||
if (![self ensureHttpsUrlPopulated:NO]) {
|
if (![self ensureHttpsUrlPopulated:NO]) {
|
||||||
return nil;
|
return nil;
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
|||||||
Log(LOG_I, @"IPv6 address chosen: %@ -> %@", [service hostName], host.ipv6Address);
|
Log(LOG_I, @"IPv6 address chosen: %@ -> %@", [service hostName], host.ipv6Address);
|
||||||
|
|
||||||
host.activeAddress = host.localAddress;
|
host.activeAddress = host.localAddress;
|
||||||
|
// [host.activeAddressPool addObject:host.localAddress];
|
||||||
host.name = service.hostName;
|
host.name = service.hostName;
|
||||||
[self.callback updateHost:host];
|
[self.callback updateHost:host];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
// If we reached this host through this port, store our port there
|
// If we reached this host through this port, store our port there
|
||||||
if (host.activeAddress && [lanAddr isEqualToString:[Utils addressPortStringToAddress:host.activeAddress]]) {
|
if (host.activeAddress && [lanAddr isEqualToString:[Utils addressPortStringToAddress:host.activeAddress]]) {
|
||||||
localPort = [Utils addressPortStringToPort:host.activeAddress];
|
localPort = [Utils addressPortStringToPort:host.activeAddress];
|
||||||
|
[host.activeAddressPool addObject:host.activeAddress];
|
||||||
}
|
}
|
||||||
else if (host.localAddress) {
|
else if (host.localAddress) {
|
||||||
// If there's an existing local address, use the port from that
|
// If there's an existing local address, use the port from that
|
||||||
@@ -57,6 +58,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
host.localAddress = [Utils addressAndPortToAddressPortString:lanAddr port:localPort];
|
host.localAddress = [Utils addressAndPortToAddressPortString:lanAddr port:localPort];
|
||||||
|
[host.activeAddressPool addObject:lanAddr];
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a Sunshine extension for WAN port remapping
|
// 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.
|
// Created by Diego Waxemberg on 1/19/14.
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// 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 "ConnectionCallbacks.h"
|
||||||
#import "VideoDecoderRenderer.h"
|
#import "VideoDecoderRenderer.h"
|
||||||
@@ -15,6 +19,8 @@
|
|||||||
#define CONN_TEST_SERVER "www.baidu.com"
|
#define CONN_TEST_SERVER "www.baidu.com"
|
||||||
|
|
||||||
@interface Connection : NSOperation <NSStreamDelegate>
|
@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;
|
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks;
|
||||||
-(void) terminate;
|
-(void) terminate;
|
||||||
@@ -23,4 +29,6 @@
|
|||||||
-(BOOL) getVideoStats:(video_stats_t*)stats;
|
-(BOOL) getVideoStats:(video_stats_t*)stats;
|
||||||
-(NSString*) getActiveCodecName;
|
-(NSString*) getActiveCodecName;
|
||||||
|
|
||||||
|
+ (void)setVolume:(float)newVolume;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
// Created by Diego Waxemberg on 1/19/14.
|
// Created by Diego Waxemberg on 1/19/14.
|
||||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
// 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 "Connection.h"
|
||||||
#import "Plot.h"
|
#import "Plot.h"
|
||||||
@@ -18,6 +21,7 @@
|
|||||||
|
|
||||||
#include "Limelight.h"
|
#include "Limelight.h"
|
||||||
#include "opus_multistream.h"
|
#include "opus_multistream.h"
|
||||||
|
#include "VoidLink-Swift.h"
|
||||||
|
|
||||||
@implementation Connection {
|
@implementation Connection {
|
||||||
SERVER_INFORMATION _serverInfo;
|
SERVER_INFORMATION _serverInfo;
|
||||||
@@ -43,15 +47,26 @@ static NSLock* videoStatsLock;
|
|||||||
static SDL_AudioDeviceID audioDevice;
|
static SDL_AudioDeviceID audioDevice;
|
||||||
static OPUS_MULTISTREAM_CONFIGURATION audioConfig;
|
static OPUS_MULTISTREAM_CONFIGURATION audioConfig;
|
||||||
static void* audioBuffer;
|
static void* audioBuffer;
|
||||||
|
static float volume = 1.0;
|
||||||
static int audioFrameSize;
|
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 VideoDecoderRenderer* renderer;
|
||||||
|
|
||||||
static BandwidthTracker *bwTracker;
|
static BandwidthTracker *bwTracker;
|
||||||
|
|
||||||
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
|
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;
|
lastFrameNumber = 0;
|
||||||
activeVideoFormat = videoFormat;
|
activeVideoFormat = videoFormat;
|
||||||
Log(LOG_I, @"Active video format: 0x%x", activeVideoFormat);
|
Log(LOG_I, @"Active video format: 0x%x", activeVideoFormat);
|
||||||
@@ -273,11 +288,27 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
|
|||||||
// Start playback
|
// Start playback
|
||||||
SDL_PauseAudioDevice(audioDevice, 0);
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,8 +332,78 @@ void ArCleanup(void)
|
|||||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
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)
|
void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||||
{
|
{
|
||||||
|
if(appDidEnterBackgroundWithoutPip && muteInBackground) return;
|
||||||
|
|
||||||
int decodeLen;
|
int decodeLen;
|
||||||
|
|
||||||
// Don't queue if there's already more than 30 ms of audio data waiting
|
// 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,
|
(float*)audioBuffer,
|
||||||
audioConfig.samplesPerFrame,
|
audioConfig.samplesPerFrame,
|
||||||
0);
|
0);
|
||||||
|
|
||||||
if (decodeLen > 0) {
|
if (decodeLen > 0) {
|
||||||
// Provide backpressure on the queue to ensure too many frames don't build up
|
// Provide backpressure on the queue to ensure too many frames don't build up
|
||||||
// in SDL's audio queue.
|
// 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,
|
else{
|
||||||
audioBuffer,
|
if(volume != 1.0){
|
||||||
sizeof(float) * decodeLen * audioConfig.channelCount) < 0) {
|
int totalSamples = decodeLen * audioConfig.channelCount;
|
||||||
Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError());
|
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,7 +535,10 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
|
|||||||
// won't be able to acquire it if LiStartConnection is in
|
// won't be able to acquire it if LiStartConnection is in
|
||||||
// progress.
|
// progress.
|
||||||
LiInterruptConnection();
|
LiInterruptConnection();
|
||||||
|
[audioPlayerNode stop];
|
||||||
|
[audioEngine stop];
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||||
|
|
||||||
// We dispatch this async to get out because this can be invoked
|
// 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
|
// on a thread inside common and we don't want to deadlock. It also avoids
|
||||||
// blocking on the caller's thread waiting to acquire initLock.
|
// blocking on the caller's thread waiting to acquire initLock.
|
||||||
@@ -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
|
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks
|
||||||
{
|
{
|
||||||
self = [super init];
|
self = [super init];
|
||||||
@@ -461,14 +617,16 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
|
|||||||
_callbacks = callbacks;
|
_callbacks = callbacks;
|
||||||
|
|
||||||
LiInitializeStreamConfiguration(&_streamConfig);
|
LiInitializeStreamConfiguration(&_streamConfig);
|
||||||
_streamConfig.colorRange = 1; // Full range
|
_streamConfig.colorRange = config.fullColorRange ? 1 : 0;
|
||||||
|
fullColorRange = config.fullColorRange;
|
||||||
_streamConfig.width = config.width;
|
_streamConfig.width = config.width;
|
||||||
_streamConfig.height = config.height;
|
_streamConfig.height = config.height;
|
||||||
_streamConfig.fps = config.frameRate;
|
_streamConfig.fps = config.frameRate;
|
||||||
_streamConfig.bitrate = config.bitRate;
|
_streamConfig.bitrate = config.bitRate;
|
||||||
_streamConfig.supportedVideoFormats = config.supportedVideoFormats;
|
_streamConfig.supportedVideoFormats = config.supportedVideoFormats;
|
||||||
_streamConfig.audioConfiguration = config.audioConfiguration;
|
_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
|
// 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
|
// on a 64-bit device with ARMv8 crypto instructions, so we don't
|
||||||
// need to check for that here.
|
// 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);
|
LiInitializeVideoCallbacks(&_drCallbacks);
|
||||||
_drCallbacks.setup = DrDecoderSetup;
|
_drCallbacks.setup = DrDecoderSetup;
|
||||||
_drCallbacks.cleanup = DrCleanup;
|
_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];
|
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 |
|
_drCallbacks.capabilities = CAPABILITY_PULL_RENDERER |
|
||||||
CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC |
|
CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC |
|
||||||
CAPABILITY_REFERENCE_FRAME_INVALIDATION_AV1;
|
CAPABILITY_REFERENCE_FRAME_INVALIDATION_AV1;
|
||||||
@@ -528,7 +687,12 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
|
|||||||
_clCallbacks.rumbleTriggers = ClRumbleTriggers;
|
_clCallbacks.rumbleTriggers = ClRumbleTriggers;
|
||||||
_clCallbacks.setMotionEventState = ClSetMotionEventState;
|
_clCallbacks.setMotionEventState = ClSetMotionEventState;
|
||||||
_clCallbacks.setControllerLED = ClSetControllerLED;
|
_clCallbacks.setControllerLED = ClSetControllerLED;
|
||||||
|
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(handleAudioSessionInterruption:)
|
||||||
|
name:AVAudioSessionInterruptionNotification
|
||||||
|
object:nil];
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
@property int serverCodecModeSupport;
|
@property int serverCodecModeSupport;
|
||||||
@property BOOL enableYUV444;
|
@property BOOL enableYUV444;
|
||||||
@property BOOL enablePIP;
|
@property BOOL enablePIP;
|
||||||
|
@property BOOL fullColorRange;
|
||||||
@property int width;
|
@property int width;
|
||||||
@property int height;
|
@property int height;
|
||||||
@property int frameRate;
|
@property int frameRate;
|
||||||
@@ -31,7 +32,9 @@
|
|||||||
@property int gamepadMask;
|
@property int gamepadMask;
|
||||||
@property BOOL optimizeGameSettings;
|
@property BOOL optimizeGameSettings;
|
||||||
@property BOOL playAudioOnPC;
|
@property BOOL playAudioOnPC;
|
||||||
|
@property BOOL redirectMic;
|
||||||
@property BOOL swapABXYButtons;
|
@property BOOL swapABXYButtons;
|
||||||
|
@property BOOL buttonVisualFeedback;
|
||||||
@property BOOL asyncNativeTouchPriority;
|
@property BOOL asyncNativeTouchPriority;
|
||||||
@property int gyroMode;
|
@property int gyroMode;
|
||||||
@property int emulatedControllerType;
|
@property int emulatedControllerType;
|
||||||
@@ -40,5 +43,6 @@
|
|||||||
@property BOOL multiController;
|
@property BOOL multiController;
|
||||||
@property NSData* serverCert;
|
@property NSData* serverCert;
|
||||||
@property int localMousePointerMode;
|
@property int localMousePointerMode;
|
||||||
|
@property CGFloat localVolume;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -9,5 +9,5 @@
|
|||||||
#import "StreamConfiguration.h"
|
#import "StreamConfiguration.h"
|
||||||
|
|
||||||
@implementation StreamConfiguration
|
@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
|
@end
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callback;
|
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callback;
|
||||||
|
|
||||||
- (void) stopStream;
|
- (void) stopStream;
|
||||||
|
- (void) setNeedRequeuing:(bool)needRequeuing;
|
||||||
|
|
||||||
- (NSString*) getStatsOverlayText: (uint16_t) overlayLevel;
|
- (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
|
- (void) stopStream
|
||||||
{
|
{
|
||||||
[_connection terminate];
|
[_connection terminate];
|
||||||
|
_callbacks = nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL) launchApp:(HttpManager*)hMan receiveSessionUrl:(NSString**)sessionUrl {
|
- (BOOL) launchApp:(HttpManager*)hMan receiveSessionUrl:(NSString**)sessionUrl {
|
||||||
@@ -169,7 +176,7 @@
|
|||||||
uint32_t rtt, variance;
|
uint32_t rtt, variance;
|
||||||
NSString* latencyString;
|
NSString* latencyString;
|
||||||
if (LiGetEstimatedRttInfo(&rtt, &variance)) {
|
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 {
|
else {
|
||||||
latencyString = @"N/A";
|
latencyString = @"N/A";
|
||||||
@@ -189,13 +196,13 @@
|
|||||||
|
|
||||||
float interval = stats.endTime - stats.startTime;
|
float interval = stats.endTime - stats.startTime;
|
||||||
|
|
||||||
// Check if we're using legacy pacing mode
|
// Get frame pacing mode
|
||||||
DataManager* dataMan = [[DataManager alloc] init];
|
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
|
// Calculate FPS differently based on pacing mode
|
||||||
float fps;
|
float fps;
|
||||||
if (isLegacyPacing) {
|
if (framePacingMode == FramePacingModeLegacy || framePacingMode == FramePacingModeOff) {
|
||||||
fps = stats.totalFrames / interval;
|
fps = stats.totalFrames / interval;
|
||||||
} else {
|
} else {
|
||||||
float scalePlotMetrics = stats.frameDropMetrics.nsamples > 0 ? ((float)stats.frameDropMetrics.nsamples / stats.totalFrames) : 1.0f;
|
float scalePlotMetrics = stats.frameDropMetrics.nsamples > 0 ? ((float)stats.frameDropMetrics.nsamples / stats.totalFrames) : 1.0f;
|
||||||
@@ -205,18 +212,25 @@
|
|||||||
double avgVideoMbps = [_connection getBwTracker].averageMbps;
|
double avgVideoMbps = [_connection getBwTracker].averageMbps;
|
||||||
double peakVideoMbps = [_connection getBwTracker].peakMbps;
|
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,
|
fps,
|
||||||
stats.networkDroppedFrames / interval,
|
stats.networkDroppedFrames / interval,
|
||||||
|
avgVideoMbps,
|
||||||
latencyString];
|
latencyString];
|
||||||
else {
|
else {
|
||||||
if (isLegacyPacing) {
|
if (framePacingMode == FramePacingModeLegacy || framePacingMode == FramePacingModeOff) {
|
||||||
NSString* rendererWithPacing = stats.renderingBackendString;
|
NSString* rendererWithPacing = stats.renderingBackendString;
|
||||||
if ([stats.renderingBackendString isEqualToString:@"AVSampleBuffer"]) {
|
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"
|
"Bitrate: %.1f Mbps, Peak: %.1f\n"
|
||||||
"%@"
|
"%@"
|
||||||
"Renderer: %@\n"
|
"Renderer: %@\n"
|
||||||
@@ -226,6 +240,7 @@
|
|||||||
_config.height,
|
_config.height,
|
||||||
fps,
|
fps,
|
||||||
[_connection getActiveCodecName],
|
[_connection getActiveCodecName],
|
||||||
|
colorRange,
|
||||||
avgVideoMbps, peakVideoMbps,
|
avgVideoMbps, peakVideoMbps,
|
||||||
hostProcessingString,
|
hostProcessingString,
|
||||||
rendererWithPacing,
|
rendererWithPacing,
|
||||||
@@ -237,7 +252,7 @@
|
|||||||
rendererWithPacing = @"AVSampleBuffer (Queue Pacing)";
|
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"
|
"Bitrate: %.1f Mbps, Peak: %.1f, Frames buffered: %.1f\n"
|
||||||
"%@"
|
"%@"
|
||||||
"Renderer: %@\n"
|
"Renderer: %@\n"
|
||||||
@@ -248,6 +263,7 @@
|
|||||||
_config.height,
|
_config.height,
|
||||||
fps,
|
fps,
|
||||||
[_connection getActiveCodecName],
|
[_connection getActiveCodecName],
|
||||||
|
colorRange,
|
||||||
avgVideoMbps, peakVideoMbps, stats.frameQueueMetrics.avg,
|
avgVideoMbps, peakVideoMbps, stats.frameQueueMetrics.avg,
|
||||||
hostProcessingString,
|
hostProcessingString,
|
||||||
rendererWithPacing,
|
rendererWithPacing,
|
||||||
|
|||||||
@@ -18,12 +18,15 @@
|
|||||||
|
|
||||||
@property (atomic, readonly) PlotMetrics decodeMetrics;
|
@property (atomic, readonly) PlotMetrics decodeMetrics;
|
||||||
@property (atomic, readonly) PlotMetrics frameQueueMetrics;
|
@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;
|
@property (nonatomic, strong, readonly) AVSampleBufferDisplayLayer *displayLayer;
|
||||||
|
|
||||||
- (id)initWithView:(UIView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio;
|
- (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)renderFrame:(Frame *)frame atTime:(CMTime)targetTime;
|
||||||
- (void)cleanup;
|
- (void)cleanup;
|
||||||
- (void)setHdrMode:(BOOL)enabled;
|
- (void)setHdrMode:(BOOL)enabled;
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
// Created by Cameron Gutman on 10/18/14.
|
// Created by Cameron Gutman on 10/18/14.
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
// Modified by True砖家 since 2026.2
|
||||||
|
// Copyright © 2026 True砖家 @ Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
@import AVFoundation;
|
@import AVFoundation;
|
||||||
@import VideoToolbox;
|
@import VideoToolbox;
|
||||||
@@ -36,12 +40,13 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
|||||||
@implementation VideoDecoderRenderer {
|
@implementation VideoDecoderRenderer {
|
||||||
dispatch_queue_t _sq, _vtq;
|
dispatch_queue_t _sq, _vtq;
|
||||||
StreamView* _view;
|
StreamView* _view;
|
||||||
id<ConnectionCallbacks> _callbacks;
|
__weak id<ConnectionCallbacks> _callbacks;
|
||||||
float _streamAspectRatio;
|
float _streamAspectRatio;
|
||||||
|
|
||||||
AVSampleBufferDisplayLayer* _displayLayer;
|
AVSampleBufferDisplayLayer* _displayLayer;
|
||||||
int _videoFormat;
|
int _videoFormat;
|
||||||
int _frameRate;
|
int _frameRate;
|
||||||
|
BOOL _fullRange;
|
||||||
|
|
||||||
NSMutableArray *_parameterSetBuffers;
|
NSMutableArray *_parameterSetBuffers;
|
||||||
NSData *_masteringDisplayColorVolume;
|
NSData *_masteringDisplayColorVolume;
|
||||||
@@ -51,11 +56,15 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
|||||||
VTDecompressionSessionRef _decompressionSession;
|
VTDecompressionSessionRef _decompressionSession;
|
||||||
|
|
||||||
CADisplayLink *_displayLink;
|
CADisplayLink *_displayLink;
|
||||||
FrameQueue *_frameQueue;
|
|
||||||
NSInteger _maxRefreshRate;
|
NSInteger _maxRefreshRate;
|
||||||
RenderingBackend _renderingBackend;
|
RenderingBackend _renderingBackend;
|
||||||
|
|
||||||
BOOL _useLegacyPacing;
|
FramePacingMode _framePacingMode;
|
||||||
|
bool _enableTimebase;
|
||||||
|
bool _asyncFrameDequeue;
|
||||||
|
|
||||||
|
// CMTime playTime;
|
||||||
|
// NSTimeInterval previousLinkTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)reinitializeDisplayLayer
|
- (void)reinitializeDisplayLayer
|
||||||
@@ -114,7 +123,9 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
|||||||
{
|
{
|
||||||
NSLog(@"initializing video decoder %f", CACurrentMediaTime());
|
NSLog(@"initializing video decoder %f", CACurrentMediaTime());
|
||||||
self = [super init];
|
self = [super init];
|
||||||
|
|
||||||
|
appDidEnterBackgroundWithoutPip = false;
|
||||||
|
|
||||||
_sq = dispatch_queue_create("com.moonlight.VideoDecoderRenderer",
|
_sq = dispatch_queue_create("com.moonlight.VideoDecoderRenderer",
|
||||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0));
|
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",
|
_vtq = dispatch_queue_create("com.moonlight.VideoDecoderRenderer.VTDecoder",
|
||||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0));
|
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0));
|
||||||
|
|
||||||
_view = view;
|
_view = (StreamView*) view;
|
||||||
_callbacks = callbacks;
|
_callbacks = callbacks;
|
||||||
_streamAspectRatio = aspectRatio;
|
_streamAspectRatio = aspectRatio;
|
||||||
_maxRefreshRate = [[UIScreen mainScreen] maximumFramesPerSecond];
|
_maxRefreshRate = [[UIScreen mainScreen] maximumFramesPerSecond];
|
||||||
_parameterSetBuffers = [[NSMutableArray alloc] init];
|
_parameterSetBuffers = [[NSMutableArray alloc] init];
|
||||||
|
|
||||||
DataManager* dataMan = [[DataManager 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 = [FrameQueue sharedInstance];
|
||||||
[_frameQueue start];
|
[_frameQueue start];
|
||||||
[_frameQueue setHighWaterMark:(int)[[dataMan getSettings].frameQueueSize integerValue]];
|
[_frameQueue setHighWaterMark:MAX(1, _queueSize)];
|
||||||
|
|
||||||
[self reinitializeDisplayLayer];
|
[self reinitializeDisplayLayer];
|
||||||
|
// NSTimeInterval interval = 1.0/tempSettings.framerate.intValue;
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
selector:@selector(reinitializeDisplayLayer)
|
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
|
# 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->_videoFormat = videoFormat;
|
||||||
self->_frameRate = frameRate;
|
self->_frameRate = frameRate;
|
||||||
|
self->_fullRange = fullRange;
|
||||||
|
|
||||||
// reset plot data in case we've already used it for a previous renderer
|
// reset plot data in case we've already used it for a previous renderer
|
||||||
[[ImGuiPlots sharedInstance] clearData];
|
[[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
|
// Choose the appropriate selector based on frame pacing mode
|
||||||
SEL displayLinkSelector;
|
SEL displayLinkSelector;
|
||||||
if (_useLegacyPacing) {
|
if (_framePacingMode == FramePacingModeLegacy || _framePacingMode == FramePacingModeOff) {
|
||||||
// Legacy frame pacing: use simple displayLinkCallback
|
// Legacy frame pacing or Off mode: use simple displayLinkCallback
|
||||||
displayLinkSelector = @selector(displayLinkCallback:);
|
displayLinkSelector = @selector(displayLinkCallback:);
|
||||||
} else {
|
} else {
|
||||||
// PACING_MODE_VSYNC:
|
// PACING_MODE_VSYNC:
|
||||||
@@ -220,10 +239,9 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
|||||||
#else
|
#else
|
||||||
NSNumber *pixelFormat = nil;
|
NSNumber *pixelFormat = nil;
|
||||||
if (self->_videoFormat & VIDEO_FORMAT_MASK_YUV444) {
|
if (self->_videoFormat & VIDEO_FORMAT_MASK_YUV444) {
|
||||||
pixelFormat = @(kCVPixelFormatType_444YpCbCr10BiPlanarFullRange);
|
pixelFormat = self->_fullRange ? @(kCVPixelFormatType_444YpCbCr10BiPlanarFullRange) : @(kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange);
|
||||||
}
|
} else {
|
||||||
else {
|
pixelFormat = self->_fullRange ? @(kCVPixelFormatType_420YpCbCr10BiPlanarFullRange) : @(kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange);
|
||||||
pixelFormat = @(kCVPixelFormatType_420YpCbCr10BiPlanarFullRange);
|
|
||||||
}
|
}
|
||||||
NSMutableDictionary *destinationPixelBufferAttributes = [@{
|
NSMutableDictionary *destinationPixelBufferAttributes = [@{
|
||||||
(id)kCVPixelBufferPixelFormatTypeKey : pixelFormat
|
(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
|
// Check for issues with the SampleBuffer, this should be much less likely since
|
||||||
// AVSB is not actually decoding the frames anymore
|
// AVSB is not actually decoding the frames anymore
|
||||||
if (self->_displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
|
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,
|
// Recreate the display layer. We are already on the main thread,
|
||||||
// so this is safe to do right here.
|
// 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
|
#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
|
// 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.
|
// 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
|
// 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 {
|
- (void)renderModeAVSB:(CADisplayLink *)link {
|
||||||
|
// NSTimeInterval current = link.targetTimestamp;
|
||||||
|
// NSLog(@"link %f", link.duration);
|
||||||
|
// previousLinkTime = current;
|
||||||
|
|
||||||
CFTimeInterval start = link.timestamp;
|
CFTimeInterval start = link.timestamp;
|
||||||
CFTimeInterval deadline = link.targetTimestamp;
|
CFTimeInterval targetTime = link.targetTimestamp;
|
||||||
static CFTimeInterval lastTargetLocal = 0.0f;
|
static CFTimeInterval lastTargetLocal = 0.0f;
|
||||||
CFTimeInterval dl0 = CACurrentMediaTime();
|
CFTimeInterval dl0 = CACurrentMediaTime();
|
||||||
|
|
||||||
|
/*
|
||||||
static int lateCallbacks = 0;
|
static int lateCallbacks = 0;
|
||||||
if (dl0 > deadline) {
|
if (dl0 > nextFrameTime) {
|
||||||
// we already missed it, count how often this happens
|
// we already missed it, count how often this happens
|
||||||
lateCallbacks++;
|
lateCallbacks++;
|
||||||
return;
|
return;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
[self checkDisplayLayer];
|
[self checkDisplayLayer];
|
||||||
|
|
||||||
static CFTimeInterval avgOverhead = 0.004f; // averaged each callback
|
CFTimeInterval waitFor = targetTime - dl0;
|
||||||
CFTimeInterval waitFor = deadline - dl0 - avgOverhead;
|
|
||||||
|
/*
|
||||||
if (waitFor < 0.001f) {
|
if (waitFor < 0.001f) {
|
||||||
|
NSLog(@"waitFor %f", waitFor);
|
||||||
|
// waitFor = isIPhone ? waitFor : 0.0f;
|
||||||
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(_needRequeuing ? _frameQueue.count>MAX(_queueSize-1,0) : true){
|
||||||
if (frame) {
|
// if(true){
|
||||||
CFTimeInterval dl1 = CACurrentMediaTime();
|
_needRequeuing = false;
|
||||||
|
if(_asyncFrameDequeue){
|
||||||
LogOnce(LOG_I, @"Frame pacing: using AVSampleBufferDisplayLayer target %f Hz with %d FPS stream", 1.0f / (deadline - start), self->_frameRate);
|
[_frameQueue dequeueWithTimeout:waitFor completion:^(Frame *frame) {
|
||||||
|
if (frame) {
|
||||||
// The system works best with properly timed video frames, which we time to the end of the next vsync period,
|
// LogOnce(LOG_I, @"Frame pacing: using AVSampleBufferDisplayLayer target %f Hz with %d FPS stream", 1.0f / (deadline - start), self->_frameRate);
|
||||||
// the earliest they can be displayed due to double-buffering.
|
[self renderFrame:frame atTime:CMTimeMakeWithSeconds(targetTime, NSEC_PER_SEC)];
|
||||||
CFTimeInterval targetLocal = deadline + link.duration;
|
}
|
||||||
|
}];
|
||||||
[self renderFrame:frame atTime:CMTimeMakeWithSeconds(targetLocal, 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);
|
||||||
|
|
||||||
|
// 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)];
|
||||||
|
|
||||||
#ifdef DISPLAYLINK_VERBOSE
|
#ifdef DISPLAYLINK_VERBOSE
|
||||||
Log(LOG_I, @"[%.3f] rendering frame %d, waitFor %.3f ms, overhead %.3f ms, lateCallbacks %d, queue size %d",
|
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]);
|
deadline, frame.frameNumber, waitFor * 1000.0, avgOverhead * 1000.0, lateCallbacks, [_frameQueue count]);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Update metrics
|
// Update metrics
|
||||||
if (lastTargetLocal != 0) {
|
if (lastTargetLocal != 0) {
|
||||||
CFTimeInterval frametime = targetLocal - lastTargetLocal;
|
CFTimeInterval frametime = targetLocal - lastTargetLocal;
|
||||||
if (frametime > deadline - start + 0.0005f) {
|
if (frametime > targetTime - start + 0.0005f) {
|
||||||
// we missed a callback
|
// we missed a callback
|
||||||
// Log(LOG_W, @"*** slow frametime %.3f ms", frametime * 1000.0);
|
// Log(LOG_W, @"*** slow frametime %.3f ms", frametime * 1000.0);
|
||||||
}
|
}
|
||||||
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) {
|
if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground) {
|
||||||
[[ImGuiPlots sharedInstance] observeFloat:PLOT_FRAMETIME value:frametime * 1000.0];
|
[[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)) {
|
while (LiPollNextVideoFrame(&handle, &du)) {
|
||||||
LiCompleteVideoFrame(handle, DrSubmitDecodeUnit(du));
|
LiCompleteVideoFrame(handle, DrSubmitDecodeUnit(du));
|
||||||
|
|
||||||
// Calculate the actual display refresh rate
|
// Skip frame pacing logic if frame pacing is off
|
||||||
double displayRefreshRate = 1 / (_displayLink.targetTimestamp - _displayLink.timestamp);
|
if (_framePacingMode != FramePacingModeOff) {
|
||||||
|
// Calculate the actual display refresh rate
|
||||||
// Only pace frames if the display refresh rate is >= 90% of our stream frame rate.
|
double displayRefreshRate = 1 / (_displayLink.targetTimestamp - _displayLink.timestamp);
|
||||||
// Battery saver, accessibility settings, or device thermals can cause the actual
|
|
||||||
// refresh rate of the display to drop below the physical maximum.
|
// Only pace frames if the display refresh rate is >= 90% of our stream frame rate.
|
||||||
if (displayRefreshRate >= _frameRate * 0.9f) {
|
// Battery saver, accessibility settings, or device thermals can cause the actual
|
||||||
// Keep one pending frame to smooth out gaps due to
|
// refresh rate of the display to drop below the physical maximum.
|
||||||
// network jitter at the cost of 1 frame of latency
|
if (displayRefreshRate >= _frameRate * 0.9f) {
|
||||||
if (LiGetPendingVideoFrames() == 1) {
|
// Keep one pending frame to smooth out gaps due to
|
||||||
break;
|
// 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 {
|
- (void)renderFrame:(Frame *)frame atTime:(CMTime)targetTime {
|
||||||
CMSampleBufferSetOutputPresentationTimeStamp(frame.sampleBuffer, 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.
|
// On first frame, set timebase to the initial presentation time.
|
||||||
// This will let us present frames using the local clock (vsync pacing) or
|
// This will let us present frames using the local clock (vsync pacing) or
|
||||||
// the pts timestamps from the host.
|
// 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);
|
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
|
#ifdef DISPLAYLINK_VERBOSE
|
||||||
// Some OS-level metrics I'm not sure what to do with
|
// 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];
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||||
[_displayLink invalidate];
|
[_displayLink invalidate];
|
||||||
}
|
}
|
||||||
- (void)cleanup
|
|
||||||
{
|
|
||||||
[_frameQueue stop];
|
|
||||||
|
|
||||||
if (_renderingBackend == RENDER_AVSB) {
|
- (void)cleanup{
|
||||||
[_displayLink invalidate];
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
}
|
[self->_frameQueue stop];
|
||||||
|
|
||||||
@synchronized(self) {
|
if (self->_renderingBackend == RENDER_AVSB) {
|
||||||
if (_decompressionSession != NULL) {
|
[self->_displayLink invalidate];
|
||||||
VTDecompressionSessionInvalidate(_decompressionSession);
|
|
||||||
CFRelease(_decompressionSession);
|
|
||||||
_decompressionSession = nil;
|
|
||||||
}
|
}
|
||||||
}
|
@synchronized(self) {
|
||||||
|
if (self->_decompressionSession != NULL) {
|
||||||
|
VTDecompressionSessionInvalidate(self->_decompressionSession);
|
||||||
|
CFRelease(self->_decompressionSession);
|
||||||
|
self->_decompressionSession = nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#define NALU_START_PREFIX_SIZE 3
|
#define NALU_START_PREFIX_SIZE 3
|
||||||
@@ -856,7 +897,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
|||||||
|
|
||||||
CMSampleBufferRef sampleBuffer;
|
CMSampleBufferRef sampleBuffer;
|
||||||
CMTime presentationTimeStamp;
|
CMTime presentationTimeStamp;
|
||||||
if (_useLegacyPacing) {
|
if (_framePacingMode == FramePacingModeLegacy || _framePacingMode == FramePacingModeOff) {
|
||||||
presentationTimeStamp = CMTimeMake(du->presentationTimeUs / 1000, 1000);
|
presentationTimeStamp = CMTimeMake(du->presentationTimeUs / 1000, 1000);
|
||||||
} else {
|
} else {
|
||||||
presentationTimeStamp = CMTimeMake((int64_t)du->rtpTimestamp, 90000);
|
presentationTimeStamp = CMTimeMake((int64_t)du->rtpTimestamp, 90000);
|
||||||
@@ -881,9 +922,10 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
|||||||
return DR_NEED_IDR;
|
return DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_useLegacyPacing) {
|
if (_framePacingMode == FramePacingModeLegacy || _framePacingMode == FramePacingModeOff) {
|
||||||
// Enqueue the next frame
|
// Enqueue the next frame
|
||||||
[self->_displayLayer enqueueSampleBuffer:sampleBuffer];
|
if(appDidEnterBackgroundWithoutPip) [self->_displayLayer flush];
|
||||||
|
else [self->_displayLayer enqueueSampleBuffer:sampleBuffer];
|
||||||
|
|
||||||
if (du->frameType == FRAME_TYPE_IDR) {
|
if (du->frameType == FRAME_TYPE_IDR) {
|
||||||
// Ensure the layer is visible now
|
// 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 "UIAppView.h"
|
||||||
#import "AppAssetManager.h"
|
#import "AppAssetManager.h"
|
||||||
#import "ThemeManager.h"
|
#import "VoidLink-Swift.h"
|
||||||
|
|
||||||
static const float REFRESH_CYCLE = 1.0f;
|
static const float REFRESH_CYCLE = 1.0f;
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ static UIImage* noImage;
|
|||||||
if (@available(iOS 13.0, *)) {
|
if (@available(iOS 13.0, *)) {
|
||||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:23];
|
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:23];
|
||||||
UIImageView* playIcon = [[UIImageView alloc] initWithImage:[[UIImage systemImageNamed:@"play.circle.fill" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
|
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];
|
playIcon.tintColor = [[UIColor blackColor] colorWithAlphaComponent:0.55];
|
||||||
|
|
||||||
_appOverlay = playIcon;
|
_appOverlay = playIcon;
|
||||||
@@ -202,7 +202,7 @@ static UIImage* noImage;
|
|||||||
UIImage* appIconImage = [[UIImage imageNamed:@"icon-pc-app"] imageWithConfiguration:config];
|
UIImage* appIconImage = [[UIImage imageNamed:@"icon-pc-app"] imageWithConfiguration:config];
|
||||||
UIImageView* appIcon = [[UIImageView alloc] initWithImage:[appIconImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
|
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];
|
appIcon.tintColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
|
||||||
|
|
||||||
_appOverlay = appIcon;
|
_appOverlay = appIcon;
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@property (nonatomic) int highWaterMark;
|
@property (nonatomic) int highWaterMark;
|
||||||
@property (nonatomic, readonly) int maxCapacity;
|
@property (nonatomic, readonly) int maxCapacity;
|
||||||
@property (atomic) BOOL paused;
|
@property (atomic) BOOL paused;
|
||||||
|
- (void)dequeueWithTimeout:(CFTimeInterval)timeout
|
||||||
|
completion:(void (^)(Frame *frame))completion;
|
||||||
|
|
||||||
+ (instancetype)sharedInstance;
|
+ (instancetype)sharedInstance;
|
||||||
|
|
||||||
@@ -23,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
- (int)enqueue:(Frame *)frame;
|
- (int)enqueue:(Frame *)frame;
|
||||||
- (int)enqueue:(Frame *)frame withSlackSize:(int)slack;
|
- (int)enqueue:(Frame *)frame withSlackSize:(int)slack;
|
||||||
- (nullable Frame *)dequeue;
|
- (nullable Frame *)dequeue;
|
||||||
- (nullable Frame *)dequeueWithTimeout:(CFTimeInterval)timeout;
|
- (nullable Frame *)dequeueWithTimeoutSync:(CFTimeInterval)timeout;
|
||||||
- (CFTimeInterval)estimatedFramerate;
|
- (CFTimeInterval)estimatedFramerate;
|
||||||
- (int)currentSoftCap;
|
- (int)currentSoftCap;
|
||||||
- (void)waitForEnqueue;
|
- (void)waitForEnqueue;
|
||||||
|
|||||||
@@ -251,7 +251,19 @@
|
|||||||
return frame;
|
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 start = CACurrentMediaTime();
|
||||||
CFTimeInterval deadline = start + timeout;
|
CFTimeInterval deadline = start + timeout;
|
||||||
int round = 0;
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||