Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20d6e221a4 | ||
|
|
2bfddf255a | ||
|
|
0e8903d233 | ||
|
|
78f385d732 | ||
|
|
bd306ba07f | ||
|
|
ececc03d46 | ||
|
|
6202a53153 | ||
|
|
84b6b22f75 | ||
|
|
f8388658d8 | ||
|
|
3c36964b5c | ||
|
|
089515443d | ||
|
|
e63e5f2120 | ||
|
|
5a5238afee | ||
|
|
e00e2f8000 | ||
|
|
8795a9f173 | ||
|
|
5bd938abc5 | ||
|
|
514dbb98e7 | ||
|
|
106b1548d4 | ||
|
|
cacf2b1951 | ||
|
|
f1a4a519c3 | ||
|
|
4f0be34947 | ||
|
|
9da1121cdb | ||
|
|
0e92af13eb | ||
|
|
cdd613cc63 | ||
|
|
58535c9997 | ||
|
|
48647d4d89 | ||
|
|
16c54ac966 | ||
|
|
4214afd2b7 | ||
|
|
d28aa43319 | ||
|
|
4983aecb4c | ||
|
|
5ba566ceee | ||
|
|
b0fd0222df | ||
|
|
7b7bb3837c | ||
|
|
6879bf6379 | ||
|
|
fc2b1689d0 | ||
|
|
9c9ef4a13f | ||
|
|
a92010f5b1 | ||
|
|
813005694a | ||
|
|
11dd542cb3 | ||
|
|
949a0e0bfb | ||
|
|
ed8a246bad | ||
|
|
cfb8946ac3 | ||
|
|
660a015f8b | ||
|
|
54ac7f6522 | ||
|
|
a01af9ee21 | ||
|
|
71b7634069 | ||
|
|
32718071e0 | ||
|
|
30d602af29 | ||
|
|
f0043a763b | ||
|
|
516648cf34 | ||
|
|
5bd9c6bd98 | ||
|
|
3d82009d4a | ||
|
|
51c13faa0c | ||
|
|
e0aff4b532 | ||
|
|
b0c16922a2 | ||
|
|
ef5d839f5e | ||
|
|
4a3ca7774e | ||
|
|
8f93cce257 | ||
|
|
9a634da3ed | ||
|
|
6460036661 | ||
|
|
c968a1df2a | ||
|
|
54c106e01e | ||
|
|
41a5855211 | ||
|
|
2652c2a99b | ||
|
|
e987965765 | ||
|
|
a7d7a7975f | ||
|
|
c79929f37e | ||
|
|
d286c8b073 | ||
|
|
2cef75fb32 | ||
|
|
ca17c1cc66 | ||
|
|
98e8305c94 | ||
|
|
45ccfa0c7f | ||
|
|
7d7b909c66 | ||
|
|
d6c1059e99 | ||
|
|
9c43e39ffb | ||
|
|
adb5091dc2 | ||
|
|
7d3c2d93ac | ||
|
|
f4b161ed06 | ||
|
|
d57334265b | ||
|
|
f7efa48ae8 | ||
|
|
deb335efc3 | ||
|
|
f039f5af29 | ||
|
|
286b5e94e6 | ||
|
|
1a719e11e6 | ||
|
|
becfc596c1 | ||
|
|
1b76d0e0a6 | ||
|
|
53209ce89e | ||
|
|
384724ff27 | ||
|
|
e2eccd7968 | ||
|
|
f762ecc3b2 | ||
|
|
fa0b864933 | ||
|
|
e0f3921a75 | ||
|
|
f698ad2fb6 | ||
|
|
a0179cc6cc | ||
|
|
511e7c0e4b | ||
|
|
e734973be1 | ||
|
|
9979408b1a | ||
|
|
a5388bb60d | ||
|
|
4c32ec59d6 | ||
|
|
af1f0e75c7 | ||
|
|
3aa7a90a2f | ||
|
|
ec956218fd | ||
|
|
02fcdcecea | ||
|
|
3575e84165 | ||
|
|
90116e7ff6 | ||
|
|
ffe48a5382 | ||
|
|
c4ddc0c4fc | ||
|
|
23299ad73d | ||
|
|
accd07f808 | ||
|
|
ed39f85508 | ||
|
|
e8349b2a6f | ||
|
|
c36051fffc | ||
|
|
5f4fe6a040 | ||
|
|
843570dbca | ||
|
|
bb44b2067e | ||
|
|
2c2088e4b1 | ||
|
|
a7088c70e5 | ||
|
|
091fd828ab | ||
|
|
d168ffbd5a | ||
|
|
0af4c70e16 | ||
|
|
e402c17fa6 | ||
|
|
ab9b2509ff | ||
|
|
535d6009ad | ||
|
|
09981df37e | ||
|
|
f7eae4cee9 | ||
|
|
ab14e18b4a | ||
|
|
1630f7987a | ||
|
|
5a9bcab368 | ||
|
|
f7b118b671 | ||
|
|
b49f4a6891 | ||
|
|
3fd0c1b397 | ||
|
|
9282c3f404 | ||
|
|
ef3fca754f | ||
|
|
c08bb232cd | ||
|
|
2d1a1573e1 | ||
|
|
92b65b4a17 | ||
|
|
f72e0f6292 | ||
|
|
57a8c22a3f | ||
|
|
4195f97d56 | ||
|
|
c734030e87 | ||
|
|
96d997d41e | ||
|
|
023cc0b583 | ||
|
|
57dac128e2 | ||
|
|
77e0048c57 | ||
|
|
fe6954cb9c | ||
|
|
237f03a501 | ||
|
|
94f61ebf0c | ||
|
|
c3a0004c68 | ||
|
|
d7aa5cc9fc | ||
|
|
26c1d068a8 | ||
|
|
35ceebfe0d | ||
|
|
5daf158908 | ||
|
|
8f12a4f598 | ||
|
|
233a23c6c8 | ||
|
|
cf48d4d7c3 | ||
|
|
5bf8eeb873 | ||
|
|
8f5d58a25c | ||
|
|
363b91b244 | ||
|
|
efc34ae1fc | ||
|
|
9e858520c8 | ||
|
|
20aba200b3 | ||
|
|
cb235d9255 | ||
|
|
b0224d502a | ||
|
|
ce67aefc3b | ||
|
|
e8d864ff66 | ||
|
|
9e2ef6cc0d | ||
|
|
9d2df484d1 | ||
|
|
d28573dcd4 | ||
|
|
c3cb31d443 | ||
|
|
b258c58d59 | ||
|
|
d8e83d1704 | ||
|
|
e78a351c28 | ||
|
|
5ee443337c | ||
|
|
4dc96ba189 | ||
|
|
ecacb1afa7 | ||
|
|
7e21292a62 | ||
|
|
9104a7b7b9 | ||
|
|
ecc7a79a8c | ||
|
|
a1a70fcca2 | ||
|
|
d769c98e22 | ||
|
|
ce59a9eb05 | ||
|
|
b2984e0af5 | ||
|
|
bed2232aff | ||
|
|
e87f0f1aa0 | ||
|
|
9807a77d9b | ||
|
|
3e1423ff46 | ||
|
|
a4f805db26 | ||
|
|
0195aeb765 | ||
|
|
fcfe83572f | ||
|
|
d61942caec | ||
|
|
515360166c | ||
|
|
5c5507b02f | ||
|
|
7e0d2af5b7 | ||
|
|
780a0e7ab8 | ||
|
|
9359333b15 | ||
|
|
819aa436e8 | ||
|
|
5beee0ffe4 | ||
|
|
30aa9a6b8d | ||
|
|
7e088acd49 | ||
|
|
f702f726b7 | ||
|
|
46834a8a8b | ||
|
|
ca69fd62d9 | ||
|
|
8fd43d47dd | ||
|
|
732ec9a9cd | ||
|
|
a0d1982a14 | ||
|
|
bb63183a51 | ||
|
|
707beddcae | ||
|
|
c4c896e43e | ||
|
|
e2acf5ac4b | ||
|
|
fcecae3a87 | ||
|
|
55a2bb7576 | ||
|
|
6b085982f1 | ||
|
|
94f1c625be | ||
|
|
b0842ea4dc | ||
|
|
49431b5eba | ||
|
|
68ee5d879b | ||
|
|
984a1acd48 | ||
|
|
00b06b7b70 | ||
|
|
84adf3ac95 | ||
|
|
9cd72271ca | ||
|
|
e71ce58d9b | ||
|
|
eb03b055ea | ||
|
|
5af39e8fb6 | ||
|
|
b12cb96416 | ||
|
|
7f4108c4e3 | ||
|
|
be48a48704 | ||
|
|
88bc5c7551 | ||
|
|
7cd857a38b | ||
|
|
60e92c458e | ||
|
|
605f276a71 | ||
|
|
210ca4190a | ||
|
|
bd2d9c05bc | ||
|
|
ae526ecddf | ||
|
|
37d7949304 | ||
|
|
6092839004 | ||
|
|
31dc13b87d | ||
|
|
dbe91f5b07 | ||
|
|
d092b93a29 | ||
|
|
eff35450f6 | ||
|
|
6204feefc0 | ||
|
|
924374bccd | ||
|
|
eac8d15240 | ||
|
|
a55d840bce | ||
|
|
22aa0a5249 | ||
|
|
c3358efa9d | ||
|
|
e59f3ba1ab | ||
|
|
42f29074fa | ||
|
|
6299401286 | ||
|
|
deba65857d | ||
|
|
691aa9c899 | ||
|
|
bd9dccd834 | ||
|
|
0dd419a5a3 | ||
|
|
2410222c19 | ||
|
|
1269c46f56 | ||
|
|
2c787a8d77 | ||
|
|
92fca9bca1 | ||
|
|
f8badd51c5 | ||
|
|
ed8bf5af9e | ||
|
|
b2521de84e | ||
|
|
4dd1aad123 | ||
|
|
165b5e6638 | ||
|
|
d8ac06ddca | ||
|
|
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 | ||
|
|
0a2fdbf0c0 | ||
|
|
91f95f13ae | ||
|
|
ebf46042ba | ||
|
|
8547cee1b9 | ||
|
|
56729469bf | ||
|
|
e619d31622 | ||
|
|
370cc76dbe | ||
|
|
1ac2f9e923 | ||
|
|
8caf360671 | ||
|
|
59b1dcfd13 | ||
|
|
34a115706e | ||
|
|
b6c029df38 | ||
|
|
31ab333f5b | ||
|
|
0582655954 | ||
|
|
c8f61b0b87 | ||
|
|
a2c4d440db | ||
|
|
b64ad64524 | ||
|
|
e9e6f1deb9 | ||
|
|
6f381362dd | ||
|
|
5d1bed4d25 | ||
|
|
9b9104b502 | ||
|
|
facbc01a9c | ||
|
|
3ba0e35032 | ||
|
|
958b391afa | ||
|
|
678c8054d7 | ||
|
|
d90657b05a | ||
|
|
9e56547435 | ||
|
|
bcb4d0a905 | ||
|
|
225b66cb06 | ||
|
|
cd1ff0da97 | ||
|
|
1d1e553191 | ||
|
|
e10691e2b7 | ||
|
|
9e20efff4b | ||
|
|
9e32c1da3d | ||
|
|
39b2621e30 | ||
|
|
4e44136f51 | ||
|
|
62bd695138 | ||
|
|
0b9454896c | ||
|
|
51c1c6db33 | ||
|
|
0c1fc66be8 | ||
|
|
ee67ff56c7 | ||
|
|
ea1a2f3cd0 | ||
|
|
25ca131da1 | ||
|
|
05f6dd86b1 | ||
|
|
2b052fef1e | ||
|
|
d6aa64b816 | ||
|
|
b3664d0e2d | ||
|
|
34eb2ccd31 | ||
|
|
0d32b8f06f | ||
|
|
73316ab5b8 | ||
|
|
69ed345191 | ||
|
|
78188527a9 | ||
|
|
2d57c3f7c1 | ||
|
|
4bfa6d270d | ||
|
|
85e6eb9eb6 | ||
|
|
83593a0ea7 | ||
|
|
dd6dbdf1aa | ||
|
|
6a2c3d7b28 | ||
|
|
15161ede06 | ||
|
|
ee370ec978 | ||
|
|
e844b5df65 | ||
|
|
087e38c9d7 | ||
|
|
2bf39aeb2e | ||
|
|
ccc85772d3 | ||
|
|
9d2f7704c7 | ||
|
|
6e20c126c3 | ||
|
|
9b149a1eb5 | ||
|
|
6010182592 | ||
|
|
868288e70f | ||
|
|
d73c0ee319 | ||
|
|
5cf9ea74f4 | ||
|
|
2ab705efdf | ||
|
|
1f4865c468 | ||
|
|
8388076b71 | ||
|
|
7a37d11aea | ||
|
|
cdd5802a72 | ||
|
|
951b7bcfb0 | ||
|
|
4cb060f496 | ||
|
|
c5da9e9bf7 | ||
|
|
386516ade9 | ||
|
|
211f90307f | ||
|
|
d6558c3619 | ||
|
|
bb265a51fd | ||
|
|
4bc8934b1b | ||
|
|
1bc15a1ba3 | ||
|
|
5026a6fbcf | ||
|
|
628ce0f12e | ||
|
|
8d05e8e670 | ||
|
|
2b11f12763 | ||
|
|
3536bf06ba | ||
|
|
88c0e358aa | ||
|
|
7b5d9cde2e | ||
|
|
c5fc72cc2c | ||
|
|
ab503875cb | ||
|
|
a9c4417434 | ||
|
|
679c0a2b4c | ||
|
|
5c1d2de9e5 | ||
|
|
1442495c9e | ||
|
|
22ce2edfa3 | ||
|
|
69026cbcd6 | ||
|
|
76f765ce5f | ||
|
|
b8ba716f34 | ||
|
|
131d115761 | ||
|
|
9b9f4f7603 | ||
|
|
c32f2d3f50 | ||
|
|
c2d3028668 | ||
|
|
338523e216 | ||
|
|
54bf701495 | ||
|
|
c1bf40d825 | ||
|
|
905e3eaf73 | ||
|
|
900a4cca14 | ||
|
|
a44e9c87d7 | ||
|
|
7b9bbba3fd | ||
|
|
fc6591f26f | ||
|
|
f50d1bd488 | ||
|
|
0c386ef337 | ||
|
|
245613162a | ||
|
|
d2eb674bb5 | ||
|
|
d5af136da2 | ||
|
|
ff8cb20cae | ||
|
|
3e48df9756 | ||
|
|
fb04850ae8 | ||
|
|
f62a405979 | ||
|
|
58180ae3fa | ||
|
|
e416dd5f78 | ||
|
|
e37c22b259 | ||
|
|
238d17020b | ||
|
|
96f11a4a69 | ||
|
|
5fcfa84766 | ||
|
|
04ecd45f7c | ||
|
|
dd9ad9728a | ||
|
|
6a3e1d8a88 | ||
|
|
d2e0cc779c | ||
|
|
ee4c1defa0 | ||
|
|
49417e7bc9 | ||
|
|
e301f2af16 | ||
|
|
0df5135f0b | ||
|
|
ab7ccc7732 | ||
|
|
02c6a12a82 | ||
|
|
ed1efdd102 | ||
|
|
e51701ef00 | ||
|
|
2b0fb3844b | ||
|
|
323613e4db | ||
|
|
80f9118c0d | ||
|
|
074a31606c | ||
|
|
3bb78db2da | ||
|
|
e5fc439da2 | ||
|
|
87ad97c00f | ||
|
|
ce21c398b5 | ||
|
|
ed56d26b1c | ||
|
|
08a2312099 | ||
|
|
619e936c39 | ||
|
|
75e88ee31e | ||
|
|
2e7abf0eb2 | ||
|
|
6941f006b3 | ||
|
|
e0d03baf1c | ||
|
|
08ef392fca | ||
|
|
b7d7b9a058 | ||
|
|
a7fc2a65e0 | ||
|
|
a707b21446 | ||
|
|
0e8980da95 | ||
|
|
ff991dd617 | ||
|
|
a8c1451a28 | ||
|
|
9198960cc7 | ||
|
|
b8478bfedc | ||
|
|
0f443af355 | ||
|
|
e63ce4d101 | ||
|
|
e57f0cd723 | ||
|
|
4fb3f7fa97 | ||
|
|
5759c122ed | ||
|
|
55ac533718 | ||
|
|
282b3d6545 | ||
|
|
eb41536749 | ||
|
|
0728959ebe | ||
|
|
b118f80b52 | ||
|
|
25073803ba | ||
|
|
1046cfe9a0 | ||
|
|
2250cba35c | ||
|
|
1e24731b60 | ||
|
|
034ee52eec | ||
|
|
13a1a4d97b | ||
|
|
f22d05a34e | ||
|
|
0d3149a696 | ||
|
|
11e5028020 | ||
|
|
2b96bb1c77 | ||
|
|
f843acf12f | ||
|
|
05d3567489 | ||
|
|
25c4ca01de | ||
|
|
ac66ffe302 | ||
|
|
f0b4e2e2ad | ||
|
|
d72930877a | ||
|
|
4d45c538cc | ||
|
|
f368076aaf | ||
|
|
61c5b958f2 | ||
|
|
334788c26f | ||
|
|
12ad168e13 | ||
|
|
48fbec02e4 | ||
|
|
ba453f9405 | ||
|
|
00491d3bce | ||
|
|
98f73928fa | ||
|
|
3dccec3739 | ||
|
|
5e40c9f9ad | ||
|
|
5bcc452c9f | ||
|
|
5bf218d930 | ||
|
|
5df330f608 | ||
|
|
bd9e2e3215 | ||
|
|
d1ecc894b3 | ||
|
|
5a2d5af887 | ||
|
|
45895fa8bf | ||
|
|
fa7710ce44 | ||
|
|
cd7d560e63 | ||
|
|
fcf0ec98e5 | ||
|
|
7691d2c0eb | ||
|
|
686418997b | ||
|
|
f4b6fd709d | ||
|
|
8126173128 | ||
|
|
5e4ba088be | ||
|
|
6682e64519 | ||
|
|
2140cc7820 | ||
|
|
7bafa39960 | ||
|
|
37379220b4 | ||
|
|
db6906d7a4 | ||
|
|
b262ae47e1 | ||
|
|
797b1a0b71 | ||
|
|
77955f6bd6 | ||
|
|
17a9fa39f5 | ||
|
|
f6032969c3 | ||
|
|
686696a6d9 | ||
|
|
c629acfa4b | ||
|
|
02ede90d77 | ||
|
|
84cb37f6f3 | ||
|
|
46526603cc | ||
|
|
80c6d488cf | ||
|
|
6ea23d94cb | ||
|
|
29c8382825 | ||
|
|
b83811cc62 | ||
|
|
47a590e92a | ||
|
|
e2ab307da7 | ||
|
|
a83274ab28 | ||
|
|
da8a210fcc | ||
|
|
0f8ba42975 | ||
|
|
35a2c1111d | ||
|
|
d263482f7e | ||
|
|
19beac795b | ||
|
|
e61e96b352 | ||
|
|
4b8691b943 | ||
|
|
487f987c25 | ||
|
|
a69d7eaede | ||
|
|
a87969d5b8 | ||
|
|
8f25081bbb | ||
|
|
6a36e73463 | ||
|
|
d69688678d | ||
|
|
56c589f042 | ||
|
|
df2b67ac99 | ||
|
|
0473233775 | ||
|
|
57b12d7a54 | ||
|
|
6c6d7b73c1 | ||
|
|
10dcdc31d8 | ||
|
|
7c725a4d80 | ||
|
|
4640ec86ed | ||
|
|
03cabd0506 | ||
|
|
879c2f368c | ||
|
|
fc73e7a3b2 | ||
|
|
b9bd1c8c44 | ||
|
|
b5ce7a8ff7 | ||
|
|
51e915b598 | ||
|
|
0addca3876 | ||
|
|
09349ebdef | ||
|
|
e93d031f86 | ||
|
|
c491132c09 | ||
|
|
0fef5a389d | ||
|
|
0047bfe51f | ||
|
|
994a60c8a4 | ||
|
|
50d53cd826 | ||
|
|
1d6906e5e7 | ||
|
|
2c8f2ee408 | ||
|
|
4810451e6a | ||
|
|
60801e3553 | ||
|
|
9bdd92aeb3 | ||
|
|
835c9c1166 | ||
|
|
9da143ed81 | ||
|
|
f7a032a918 | ||
|
|
eacdda384a | ||
|
|
1f13fd691b | ||
|
|
73de0877a1 | ||
|
|
616c7673a4 | ||
|
|
489e60d2b1 | ||
|
|
af2a2728f8 | ||
|
|
91f012ee9f | ||
|
|
c4bc77db05 | ||
|
|
6579ea681e | ||
|
|
07b6aa099b | ||
|
|
e10a296305 | ||
|
|
c42fe94b46 | ||
|
|
1d0743cda6 | ||
|
|
0f6f105ec1 | ||
|
|
578ad81b8a | ||
|
|
3cbdba78d6 | ||
|
|
1de7592e86 | ||
|
|
6a9891adee | ||
|
|
29700e6f18 | ||
|
|
7426e3e78c | ||
|
|
724a11ae4f | ||
|
|
03d08a2c65 | ||
|
|
f24e68d0d9 | ||
|
|
a07e94ca74 | ||
|
|
14a384436c | ||
|
|
6be5aa6507 | ||
|
|
97d17f505b | ||
|
|
c2ed8c5517 | ||
|
|
ac299287e4 | ||
|
|
ec5aec2601 | ||
|
|
59033ff8d7 | ||
|
|
88edc7f263 | ||
|
|
19882a9373 | ||
|
|
620385501f | ||
|
|
d0eb864814 | ||
|
|
1920735efa | ||
|
|
7370fa5c94 | ||
|
|
a875a65a7f | ||
|
|
88414cd3db | ||
|
|
00aff53e35 | ||
|
|
62561ac423 | ||
|
|
d26cef7ef4 | ||
|
|
527fb16428 | ||
|
|
0f7ba20e65 | ||
|
|
94e9bc35b1 | ||
|
|
b3fd9e51d7 | ||
|
|
bef2456bea | ||
|
|
25f65c3596 | ||
|
|
8260624a41 | ||
|
|
7ab1c43588 | ||
|
|
7d0477204b | ||
|
|
8f1dda44b9 | ||
|
|
ee03f6b866 | ||
|
|
9e41537554 | ||
|
|
8baa2e9067 | ||
|
|
556db88ff7 | ||
|
|
9063331d38 | ||
|
|
594906c121 | ||
|
|
357a907838 | ||
|
|
6bc142a07b | ||
|
|
cd6e08956c | ||
|
|
9937b67509 | ||
|
|
c478d641c6 | ||
|
|
1702f37d3e | ||
|
|
e1920ddb01 | ||
|
|
9b5b93dc31 | ||
|
|
cd4c5bbc56 | ||
|
|
47bae92dd5 | ||
|
|
a5bbca55a2 | ||
|
|
a4f5c4f211 | ||
|
|
9c61e5725f | ||
|
|
8c4f2e46f7 | ||
|
|
76253a9583 | ||
|
|
5653215f68 | ||
|
|
1b9e9575f1 | ||
|
|
dfde770274 | ||
|
|
1737eae39b | ||
|
|
fb32ef3629 | ||
|
|
5734a0a0b7 | ||
|
|
758a888a53 | ||
|
|
ed0045afec | ||
|
|
de5fb7a19b | ||
|
|
e2bebbcc61 | ||
|
|
24fd69e7dc | ||
|
|
16b1f9164f | ||
|
|
51a62ce1d8 | ||
|
|
23b68b2d15 | ||
|
|
7ee449c0e4 | ||
|
|
2891beef5f | ||
|
|
da47a6ae46 | ||
|
|
96145baa7a | ||
|
|
ab31ab393a | ||
|
|
38ee17a852 | ||
|
|
78be18ed89 | ||
|
|
5fd8a7c587 | ||
|
|
79a075e338 | ||
|
|
5c3a6de2b7 | ||
|
|
5a77571d6f | ||
|
|
1e6a380f99 | ||
|
|
8e00552c61 | ||
|
|
fabd01ca9e | ||
|
|
ab2efcf05a | ||
|
|
dfaa4005bb | ||
|
|
a64a29ce07 | ||
|
|
68f21fa8e5 | ||
|
|
d9dbb2bcee | ||
|
|
782ac4c362 | ||
|
|
ff68114fe3 | ||
|
|
cbf48bdf0a | ||
|
|
fcb68bc54f | ||
|
|
a591380d60 | ||
|
|
822dfc433b | ||
|
|
17ecd52755 | ||
|
|
6fd35deaa4 | ||
|
|
d80c2e35ca | ||
|
|
7ed6c559e0 | ||
|
|
e5b1eda3a1 | ||
|
|
3120cd785b | ||
|
|
b036e01afc | ||
|
|
01e379eefe | ||
|
|
17a00a1604 | ||
|
|
59660840e4 | ||
|
|
2775ca09c7 | ||
|
|
8c8fea0d4b | ||
|
|
7f23c4a508 | ||
|
|
fda5c8610c | ||
|
|
049034e5a9 | ||
|
|
6ae3beb2a5 | ||
|
|
13853449d2 | ||
|
|
2679ba75af | ||
|
|
561080568c | ||
|
|
4edf1e2cf5 | ||
|
|
e7998c3559 | ||
|
|
8f674d6a3d | ||
|
|
f1a9fd2b4f | ||
|
|
46f75f0676 | ||
|
|
d3b62e38fc | ||
|
|
c8ebdc0a88 | ||
|
|
238eb816d0 | ||
|
|
0f054bb96f | ||
|
|
711996b478 | ||
|
|
a43fe609d3 | ||
|
|
2712922ce5 | ||
|
|
49b685b3ec | ||
|
|
302606564c | ||
|
|
1fc1b5bc60 | ||
|
|
f260d75372 | ||
|
|
a03a50d238 | ||
|
|
928284990b | ||
|
|
03046988c7 | ||
|
|
c860433065 | ||
|
|
5d66c17bc1 | ||
|
|
16b5df7dd1 | ||
|
|
589c025951 | ||
|
|
7fbfe94b71 | ||
|
|
27bb8fd737 | ||
|
|
cc285d5488 | ||
|
|
c8522de476 | ||
|
|
94f2e39367 | ||
|
|
bd83af8bf4 | ||
|
|
420eebf831 | ||
|
|
e0691dcd97 | ||
|
|
24442c29bb | ||
|
|
871315c2bc | ||
|
|
7b116b936c | ||
|
|
9aa84c03d7 | ||
|
|
471325a358 | ||
|
|
978ab18cee | ||
|
|
ea0c16e334 | ||
|
|
dd0ff298d5 | ||
|
|
eca0b30728 | ||
|
|
9ade4b5ae0 | ||
|
|
4f8c5189e0 | ||
|
|
0f2d54e8c0 |
No files matched your search
@@ -0,0 +1,21 @@
|
|||||||
|
BasedOnStyle: Chromium
|
||||||
|
AlignAfterOpenBracket: true
|
||||||
|
AlignOperands: false
|
||||||
|
AllowAllParametersOfDeclarationOnNextLine: true
|
||||||
|
AlwaysBreakTemplateDeclarations: true
|
||||||
|
BinPackArguments: false
|
||||||
|
BinPackParameters: false
|
||||||
|
BreakBeforeTernaryOperators: true
|
||||||
|
ColumnLimit: 160
|
||||||
|
ContinuationIndentWidth: 4
|
||||||
|
DerivePointerAlignment: false
|
||||||
|
IndentWidth: 4
|
||||||
|
ObjCBlockIndentWidth: 4
|
||||||
|
ObjCSpaceAfterProperty: true
|
||||||
|
ObjCSpaceBeforeProtocolList: true
|
||||||
|
PointerBindsToType: false
|
||||||
|
PointerAlignment: Right
|
||||||
|
SpacesInContainerLiterals: true
|
||||||
|
TabWidth: 4
|
||||||
|
UseTab: Never
|
||||||
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
name: Build IPAs
|
name: Build VoidLink IPA
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "noWaterMark" ]
|
branches: [ "feat/frame_pacing_metal" ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build IPAs
|
name: Build VoidLink IPA
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -16,30 +16,15 @@ jobs:
|
|||||||
- name: Checkout submodules
|
- name: Checkout submodules
|
||||||
run: "git submodule update --init --recursive"
|
run: "git submodule update --init --recursive"
|
||||||
|
|
||||||
- name: Build FullScreen Version
|
- name: Build VoidLink
|
||||||
run: |
|
run: |
|
||||||
plutil -replace UIRequiresFullScreen -bool true Limelight/Limelight-Info.plist
|
xcodebuild -verbose -scheme VoidLink -sdk iphoneos -configuration Debug archive -archivePath $PWD/build/VoidLink.xcarchive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED="NO" CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED="NO"
|
||||||
xcodebuild -verbose -scheme Moonlight-ZWM -sdk iphoneos -configuration AppStoreDistribution archive -archivePath $PWD/build/MoonlightFullScreen.xcarchive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED="NO" CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED="NO"
|
mkdir -p ./build/IPA && mkdir -p ./build/IPA/Payload
|
||||||
mkdir -p ./build/IPA_FullScreen && mkdir -p ./build/IPA_FullScreen/Payload
|
cp -r ./build/VoidLink.xcarchive/Products/Applications/VoidLink.app ./build/IPA/Payload/VoidLink.app
|
||||||
cp -r ./build/MoonlightFullScreen.xcarchive/Products/Applications/Moonlight-ZWM.app ./build/IPA_FullScreen/Payload/Moonlight-ZWM.app
|
cd ./build/IPA && zip -r ../../build/VoidLink.ipa ./Payload/* && cd ../..
|
||||||
cd ./build/IPA_FullScreen && zip -r ../../build/Moonlight-ZWM_FullScreen.ipa ./Payload/* && cd ../..
|
|
||||||
|
|
||||||
- name: Upload FullScreen IPA to Artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: Moonlight-ZWM_FullScreen.ipa
|
|
||||||
path: build/Moonlight-ZWM_FullScreen.ipa
|
|
||||||
|
|
||||||
- name: Build StageManager Version
|
- name: Upload VoidLink IPA to Artifacts
|
||||||
run: |
|
|
||||||
plutil -replace UIRequiresFullScreen -bool false Limelight/Limelight-Info.plist
|
|
||||||
xcodebuild -verbose -scheme Moonlight-ZWM -sdk iphoneos -configuration AppStoreDistribution archive -archivePath $PWD/build/MoonlightStageManager.xcarchive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED="NO" CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED="NO"
|
|
||||||
mkdir -p ./build/IPA_StageManager && mkdir -p ./build/IPA_StageManager/Payload
|
|
||||||
cp -r ./build/MoonlightStageManager.xcarchive/Products/Applications/Moonlight-ZWM.app ./build/IPA_StageManager/Payload/Moonlight-ZWM.app
|
|
||||||
cd ./build/IPA_StageManager && zip -r ../../build/Moonlight-ZWM_StageManager.ipa ./Payload/* && cd ../..
|
|
||||||
|
|
||||||
- name: Upload StageManager IPA to Artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Moonlight-ZWM_StageManager.ipa
|
name: VoidLink.ipa
|
||||||
path: build/Moonlight-ZWM_StageManager.ipa
|
path: build/VoidLink.ipa
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
**/xcuserdata/
|
**/xcuserdata/
|
||||||
**/Moonlight.xcscmblueprint
|
**/Moonlight.xcscmblueprint
|
||||||
**/xcschemes/*.xcscheme
|
**/xcschemes/*.xcscheme
|
||||||
|
.DS_Store
|
||||||
Build
|
Build
|
||||||
DerivedData
|
DerivedData
|
||||||
|
build-home/
|
||||||
*.xccheckout
|
*.xccheckout
|
||||||
|
.github
|
||||||
|
README.md
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
[submodule "moonlight-common/moonlight-common-c"]
|
[submodule "moonlight-common/moonlight-common-c"]
|
||||||
path = moonlight-common/moonlight-common-c
|
path = moonlight-common/moonlight-common-c
|
||||||
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
url=https://github.com/TrueZhuangJia/voidlink-c.git
|
||||||
[submodule "X1Kit"]
|
[submodule "X1Kit"]
|
||||||
path = X1Kit
|
path = X1Kit
|
||||||
url = https://github.com/cgutman/X1Kit.git
|
url = https://github.com/cgutman/X1Kit.git
|
||||||
|
[submodule "third_party/imgui"]
|
||||||
|
path = third_party/imgui
|
||||||
|
url = https://github.com/ocornut/imgui.git
|
||||||
|
[submodule "third_party/moonlight-audio-haptics"]
|
||||||
|
path = third_party/moonlight-audio-haptics
|
||||||
|
url = https://github.com/TrueZhuangJia/moonlight-audio-haptics.git
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// dear imgui: Renderer Backend for Metal
|
||||||
|
// This needs to be used along with a Platform Backend (e.g. OSX)
|
||||||
|
|
||||||
|
// Implemented features:
|
||||||
|
// [X] Renderer: User texture binding. Use 'MTLTexture' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef!
|
||||||
|
// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
|
||||||
|
// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures).
|
||||||
|
|
||||||
|
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||||
|
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||||
|
// Learn about Dear ImGui:
|
||||||
|
// - FAQ https://dearimgui.com/faq
|
||||||
|
// - Getting Started https://dearimgui.com/getting-started
|
||||||
|
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||||
|
// - Introduction, links and more at the top of imgui.cpp
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "imgui.h" // IMGUI_IMPL_API
|
||||||
|
#ifndef IMGUI_DISABLE
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// ObjC API
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#ifdef __OBJC__
|
||||||
|
|
||||||
|
@class MTLRenderPassDescriptor;
|
||||||
|
@protocol MTLDevice, MTLCommandBuffer, MTLRenderCommandEncoder;
|
||||||
|
|
||||||
|
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
|
||||||
|
IMGUI_IMPL_API bool ImGui_ImplMetal_Init(id<MTLDevice> device);
|
||||||
|
IMGUI_IMPL_API void ImGui_ImplMetal_Shutdown();
|
||||||
|
IMGUI_IMPL_API void ImGui_ImplMetal_NewFrame(MTLRenderPassDescriptor* renderPassDescriptor);
|
||||||
|
IMGUI_IMPL_API void ImGui_ImplMetal_RenderDrawData(ImDrawData* drawData,
|
||||||
|
id<MTLCommandBuffer> commandBuffer,
|
||||||
|
id<MTLRenderCommandEncoder> commandEncoder);
|
||||||
|
|
||||||
|
// Called by Init/NewFrame/Shutdown
|
||||||
|
IMGUI_IMPL_API bool ImGui_ImplMetal_CreateDeviceObjects(id<MTLDevice> device);
|
||||||
|
IMGUI_IMPL_API void ImGui_ImplMetal_DestroyDeviceObjects();
|
||||||
|
|
||||||
|
// (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = NULL to handle this manually.
|
||||||
|
IMGUI_IMPL_API void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// C++ API
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Enable Metal C++ binding support with '#define IMGUI_IMPL_METAL_CPP' in your imconfig.h file
|
||||||
|
// More info about using Metal from C++: https://developer.apple.com/metal/cpp/
|
||||||
|
|
||||||
|
#ifdef IMGUI_IMPL_METAL_CPP
|
||||||
|
#include <Metal/Metal.hpp>
|
||||||
|
#ifndef __OBJC__
|
||||||
|
|
||||||
|
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
|
||||||
|
IMGUI_IMPL_API bool ImGui_ImplMetal_Init(MTL::Device* device);
|
||||||
|
IMGUI_IMPL_API void ImGui_ImplMetal_Shutdown();
|
||||||
|
IMGUI_IMPL_API void ImGui_ImplMetal_NewFrame(MTL::RenderPassDescriptor* renderPassDescriptor);
|
||||||
|
IMGUI_IMPL_API void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data,
|
||||||
|
MTL::CommandBuffer* commandBuffer,
|
||||||
|
MTL::RenderCommandEncoder* commandEncoder);
|
||||||
|
|
||||||
|
// Called by Init/NewFrame/Shutdown
|
||||||
|
IMGUI_IMPL_API bool ImGui_ImplMetal_CreateDeviceObjects(MTL::Device* device);
|
||||||
|
IMGUI_IMPL_API void ImGui_ImplMetal_DestroyDeviceObjects();
|
||||||
|
|
||||||
|
// (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = NULL to handle this manually.
|
||||||
|
IMGUI_IMPL_API void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#endif // #ifndef IMGUI_DISABLE
|
||||||
@@ -0,0 +1,657 @@
|
|||||||
|
// dear imgui: Renderer Backend for Metal
|
||||||
|
// This needs to be used along with a Platform Backend (e.g. OSX)
|
||||||
|
|
||||||
|
// Implemented features:
|
||||||
|
// [X] Renderer: User texture binding. Use 'MTLTexture' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef!
|
||||||
|
// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
|
||||||
|
// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures).
|
||||||
|
|
||||||
|
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||||
|
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||||
|
// Learn about Dear ImGui:
|
||||||
|
// - FAQ https://dearimgui.com/faq
|
||||||
|
// - Getting Started https://dearimgui.com/getting-started
|
||||||
|
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||||
|
// - Introduction, links and more at the top of imgui.cpp
|
||||||
|
|
||||||
|
// CHANGELOG
|
||||||
|
// (minor and older changes stripped away, please see git history for details)
|
||||||
|
// 2025-06-11: Added support for ImGuiBackendFlags_RendererHasTextures, for dynamic font atlas. Removed ImGui_ImplMetal_CreateFontsTexture() and ImGui_ImplMetal_DestroyFontsTexture().
|
||||||
|
// 2025-02-03: Metal: Crash fix. (#8367)
|
||||||
|
// 2024-01-08: Metal: Fixed memory leaks when using metal-cpp (#8276, #8166) or when using multiple contexts (#7419).
|
||||||
|
// 2022-08-23: Metal: Update deprecated property 'sampleCount'->'rasterSampleCount'.
|
||||||
|
// 2022-07-05: Metal: Add dispatch synchronization.
|
||||||
|
// 2022-06-30: Metal: Use __bridge for ARC based systems.
|
||||||
|
// 2022-06-01: Metal: Fixed null dereference on exit inside command buffer completion handler.
|
||||||
|
// 2022-04-27: Misc: Store backend data in a per-context struct, allowing to use this backend with multiple contexts.
|
||||||
|
// 2022-01-03: Metal: Ignore ImDrawCmd where ElemCount == 0 (very rare but can technically be manufactured by user code).
|
||||||
|
// 2021-12-30: Metal: Added Metal C++ support. Enable with '#define IMGUI_IMPL_METAL_CPP' in your imconfig.h file.
|
||||||
|
// 2021-08-24: Metal: Fixed a crash when clipping rect larger than framebuffer is submitted. (#4464)
|
||||||
|
// 2021-05-19: Metal: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement)
|
||||||
|
// 2021-02-18: Metal: Change blending equation to preserve alpha in output buffer.
|
||||||
|
// 2021-01-25: Metal: Fixed texture storage mode when building on Mac Catalyst.
|
||||||
|
// 2019-05-29: Metal: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag.
|
||||||
|
// 2019-04-30: Metal: Added support for special ImDrawCallback_ResetRenderState callback to reset render state.
|
||||||
|
// 2019-02-11: Metal: Projecting clipping rectangles correctly using draw_data->FramebufferScale to allow multi-viewports for retina display.
|
||||||
|
// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window.
|
||||||
|
// 2018-07-05: Metal: Added new Metal backend implementation.
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#ifndef IMGUI_DISABLE
|
||||||
|
#include "imgui_impl_metal.h"
|
||||||
|
#import <time.h>
|
||||||
|
#import <Metal/Metal.h>
|
||||||
|
|
||||||
|
#pragma mark - Support classes
|
||||||
|
|
||||||
|
// A wrapper around a MTLBuffer object that knows the last time it was reused
|
||||||
|
@interface MetalBuffer : NSObject
|
||||||
|
@property (nonatomic, strong) id<MTLBuffer> buffer;
|
||||||
|
@property (nonatomic, assign) double lastReuseTime;
|
||||||
|
- (instancetype)initWithBuffer:(id<MTLBuffer>)buffer;
|
||||||
|
@end
|
||||||
|
|
||||||
|
// An object that encapsulates the data necessary to uniquely identify a
|
||||||
|
// render pipeline state. These are used as cache keys.
|
||||||
|
@interface FramebufferDescriptor : NSObject<NSCopying>
|
||||||
|
@property (nonatomic, assign) unsigned long sampleCount;
|
||||||
|
@property (nonatomic, assign) MTLPixelFormat colorPixelFormat;
|
||||||
|
@property (nonatomic, assign) MTLPixelFormat depthPixelFormat;
|
||||||
|
@property (nonatomic, assign) MTLPixelFormat stencilPixelFormat;
|
||||||
|
- (instancetype)initWithRenderPassDescriptor:(MTLRenderPassDescriptor*)renderPassDescriptor;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface MetalTexture : NSObject
|
||||||
|
@property (nonatomic, strong) id<MTLTexture> metalTexture;
|
||||||
|
- (instancetype)initWithTexture:(id<MTLTexture>)metalTexture;
|
||||||
|
@end
|
||||||
|
|
||||||
|
// A singleton that stores long-lived objects that are needed by the Metal
|
||||||
|
// renderer backend. Stores the render pipeline state cache and the default
|
||||||
|
// font texture, and manages the reusable buffer cache.
|
||||||
|
@interface MetalContext : NSObject
|
||||||
|
@property (nonatomic, strong) id<MTLDevice> device;
|
||||||
|
@property (nonatomic, strong) id<MTLDepthStencilState> depthStencilState;
|
||||||
|
@property (nonatomic, strong) FramebufferDescriptor* framebufferDescriptor; // framebuffer descriptor for current frame; transient
|
||||||
|
@property (nonatomic, strong) NSMutableDictionary* renderPipelineStateCache; // pipeline cache; keyed on framebuffer descriptors
|
||||||
|
@property (nonatomic, strong) NSMutableArray<MetalBuffer*>* bufferCache;
|
||||||
|
@property (nonatomic, assign) double lastBufferCachePurge;
|
||||||
|
- (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTLDevice>)device;
|
||||||
|
- (id<MTLRenderPipelineState>)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id<MTLDevice>)device;
|
||||||
|
@end
|
||||||
|
|
||||||
|
struct ImGui_ImplMetal_Data
|
||||||
|
{
|
||||||
|
MetalContext* SharedMetalContext;
|
||||||
|
|
||||||
|
ImGui_ImplMetal_Data() { memset((void*)this, 0, sizeof(*this)); }
|
||||||
|
};
|
||||||
|
|
||||||
|
static ImGui_ImplMetal_Data* ImGui_ImplMetal_GetBackendData() { return ImGui::GetCurrentContext() ? (ImGui_ImplMetal_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; }
|
||||||
|
static void ImGui_ImplMetal_DestroyBackendData(){ IM_DELETE(ImGui_ImplMetal_GetBackendData()); }
|
||||||
|
|
||||||
|
// XXX Work around missing clock symbols on iOS. The NSDate version is the method this function used previously.
|
||||||
|
#if defined(CLOCK_UPTIME_RAW)
|
||||||
|
static inline CFTimeInterval GetMachAbsoluteTimeInSeconds() { return (CFTimeInterval)(double)(clock_gettime_nsec_np(CLOCK_UPTIME_RAW) / 1e9); }
|
||||||
|
#else
|
||||||
|
static inline CFTimeInterval GetMachAbsoluteTimeInSeconds() { return (CFTimeInterval)[NSDate date].timeIntervalSince1970; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef IMGUI_IMPL_METAL_CPP
|
||||||
|
|
||||||
|
#pragma mark - Dear ImGui Metal C++ Backend API
|
||||||
|
|
||||||
|
bool ImGui_ImplMetal_Init(MTL::Device* device)
|
||||||
|
{
|
||||||
|
return ImGui_ImplMetal_Init((__bridge id<MTLDevice>)(device));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGui_ImplMetal_NewFrame(MTL::RenderPassDescriptor* renderPassDescriptor)
|
||||||
|
{
|
||||||
|
ImGui_ImplMetal_NewFrame((__bridge MTLRenderPassDescriptor*)(renderPassDescriptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data,
|
||||||
|
MTL::CommandBuffer* commandBuffer,
|
||||||
|
MTL::RenderCommandEncoder* commandEncoder)
|
||||||
|
{
|
||||||
|
ImGui_ImplMetal_RenderDrawData(draw_data,
|
||||||
|
(__bridge id<MTLCommandBuffer>)(commandBuffer),
|
||||||
|
(__bridge id<MTLRenderCommandEncoder>)(commandEncoder));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImGui_ImplMetal_CreateDeviceObjects(MTL::Device* device)
|
||||||
|
{
|
||||||
|
return ImGui_ImplMetal_CreateDeviceObjects((__bridge id<MTLDevice>)(device));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // #ifdef IMGUI_IMPL_METAL_CPP
|
||||||
|
|
||||||
|
#pragma mark - Dear ImGui Metal Backend API
|
||||||
|
|
||||||
|
bool ImGui_ImplMetal_Init(id<MTLDevice> device)
|
||||||
|
{
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
IMGUI_CHECKVERSION();
|
||||||
|
IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!");
|
||||||
|
|
||||||
|
ImGui_ImplMetal_Data* bd = IM_NEW(ImGui_ImplMetal_Data)();
|
||||||
|
io.BackendRendererUserData = (void*)bd;
|
||||||
|
io.BackendRendererName = "imgui_impl_metal";
|
||||||
|
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
|
||||||
|
io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; // We can honor ImGuiPlatformIO::Textures[] requests during render.
|
||||||
|
|
||||||
|
bd->SharedMetalContext = [[MetalContext alloc] init];
|
||||||
|
bd->SharedMetalContext.device = device;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGui_ImplMetal_Shutdown()
|
||||||
|
{
|
||||||
|
ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
|
||||||
|
IM_UNUSED(bd);
|
||||||
|
IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?");
|
||||||
|
ImGui_ImplMetal_DestroyDeviceObjects();
|
||||||
|
ImGui_ImplMetal_DestroyBackendData();
|
||||||
|
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
io.BackendRendererName = nullptr;
|
||||||
|
io.BackendRendererUserData = nullptr;
|
||||||
|
io.BackendFlags &= ~(ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_RendererHasTextures);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGui_ImplMetal_NewFrame(MTLRenderPassDescriptor* renderPassDescriptor)
|
||||||
|
{
|
||||||
|
ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
|
||||||
|
IM_ASSERT(bd != nil && "Context or backend not initialized! Did you call ImGui_ImplMetal_Init()?");
|
||||||
|
#ifdef IMGUI_IMPL_METAL_CPP
|
||||||
|
bd->SharedMetalContext.framebufferDescriptor = [[[FramebufferDescriptor alloc] initWithRenderPassDescriptor:renderPassDescriptor]autorelease];
|
||||||
|
#else
|
||||||
|
bd->SharedMetalContext.framebufferDescriptor = [[FramebufferDescriptor alloc] initWithRenderPassDescriptor:renderPassDescriptor];
|
||||||
|
#endif
|
||||||
|
if (bd->SharedMetalContext.depthStencilState == nil)
|
||||||
|
ImGui_ImplMetal_CreateDeviceObjects(bd->SharedMetalContext.device);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ImGui_ImplMetal_SetupRenderState(ImDrawData* draw_data, id<MTLCommandBuffer> commandBuffer,
|
||||||
|
id<MTLRenderCommandEncoder> commandEncoder, id<MTLRenderPipelineState> renderPipelineState,
|
||||||
|
MetalBuffer* vertexBuffer, size_t vertexBufferOffset)
|
||||||
|
{
|
||||||
|
IM_UNUSED(commandBuffer);
|
||||||
|
ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
|
||||||
|
[commandEncoder setCullMode:MTLCullModeNone];
|
||||||
|
[commandEncoder setDepthStencilState:bd->SharedMetalContext.depthStencilState];
|
||||||
|
|
||||||
|
// Setup viewport, orthographic projection matrix
|
||||||
|
// Our visible imgui space lies from draw_data->DisplayPos (top left) to
|
||||||
|
// draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayMin is typically (0,0) for single viewport apps.
|
||||||
|
MTLViewport viewport =
|
||||||
|
{
|
||||||
|
.originX = 0.0,
|
||||||
|
.originY = 0.0,
|
||||||
|
.width = (double)(draw_data->DisplaySize.x * draw_data->FramebufferScale.x),
|
||||||
|
.height = (double)(draw_data->DisplaySize.y * draw_data->FramebufferScale.y),
|
||||||
|
.znear = 0.0,
|
||||||
|
.zfar = 1.0
|
||||||
|
};
|
||||||
|
[commandEncoder setViewport:viewport];
|
||||||
|
|
||||||
|
float L = draw_data->DisplayPos.x;
|
||||||
|
float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x;
|
||||||
|
float T = draw_data->DisplayPos.y;
|
||||||
|
float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y;
|
||||||
|
float N = (float)viewport.znear;
|
||||||
|
float F = (float)viewport.zfar;
|
||||||
|
const float ortho_projection[4][4] =
|
||||||
|
{
|
||||||
|
{ 2.0f/(R-L), 0.0f, 0.0f, 0.0f },
|
||||||
|
{ 0.0f, 2.0f/(T-B), 0.0f, 0.0f },
|
||||||
|
{ 0.0f, 0.0f, 1/(F-N), 0.0f },
|
||||||
|
{ (R+L)/(L-R), (T+B)/(B-T), N/(F-N), 1.0f },
|
||||||
|
};
|
||||||
|
[commandEncoder setVertexBytes:&ortho_projection length:sizeof(ortho_projection) atIndex:1];
|
||||||
|
|
||||||
|
[commandEncoder setRenderPipelineState:renderPipelineState];
|
||||||
|
|
||||||
|
[commandEncoder setVertexBuffer:vertexBuffer.buffer offset:0 atIndex:0];
|
||||||
|
[commandEncoder setVertexBufferOffset:vertexBufferOffset atIndex:0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metal Render function.
|
||||||
|
void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, id<MTLCommandBuffer> commandBuffer, id<MTLRenderCommandEncoder> commandEncoder)
|
||||||
|
{
|
||||||
|
ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
|
||||||
|
MetalContext* ctx = bd->SharedMetalContext;
|
||||||
|
|
||||||
|
// Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates)
|
||||||
|
int fb_width = (int)(draw_data->DisplaySize.x * draw_data->FramebufferScale.x);
|
||||||
|
int fb_height = (int)(draw_data->DisplaySize.y * draw_data->FramebufferScale.y);
|
||||||
|
if (fb_width <= 0 || fb_height <= 0 || draw_data->CmdListsCount == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Catch up with texture updates. Most of the times, the list will have 1 element with an OK status, aka nothing to do.
|
||||||
|
// (This almost always points to ImGui::GetPlatformIO().Textures[] but is part of ImDrawData to allow overriding or disabling texture updates).
|
||||||
|
if (draw_data->Textures != nullptr)
|
||||||
|
for (ImTextureData* tex : *draw_data->Textures)
|
||||||
|
if (tex->Status != ImTextureStatus_OK)
|
||||||
|
ImGui_ImplMetal_UpdateTexture(tex);
|
||||||
|
|
||||||
|
// Try to retrieve a render pipeline state that is compatible with the framebuffer config for this frame
|
||||||
|
// The hit rate for this cache should be very near 100%.
|
||||||
|
id<MTLRenderPipelineState> renderPipelineState = ctx.renderPipelineStateCache[ctx.framebufferDescriptor];
|
||||||
|
if (renderPipelineState == nil)
|
||||||
|
{
|
||||||
|
// No luck; make a new render pipeline state
|
||||||
|
renderPipelineState = [ctx renderPipelineStateForFramebufferDescriptor:ctx.framebufferDescriptor device:commandBuffer.device];
|
||||||
|
|
||||||
|
// Cache render pipeline state for later reuse
|
||||||
|
ctx.renderPipelineStateCache[ctx.framebufferDescriptor] = renderPipelineState;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t vertexBufferLength = (size_t)draw_data->TotalVtxCount * sizeof(ImDrawVert);
|
||||||
|
size_t indexBufferLength = (size_t)draw_data->TotalIdxCount * sizeof(ImDrawIdx);
|
||||||
|
MetalBuffer* vertexBuffer = [ctx dequeueReusableBufferOfLength:vertexBufferLength device:commandBuffer.device];
|
||||||
|
MetalBuffer* indexBuffer = [ctx dequeueReusableBufferOfLength:indexBufferLength device:commandBuffer.device];
|
||||||
|
|
||||||
|
ImGui_ImplMetal_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, 0);
|
||||||
|
|
||||||
|
// Will project scissor/clipping rectangles into framebuffer space
|
||||||
|
ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports
|
||||||
|
ImVec2 clip_scale = draw_data->FramebufferScale; // (1,1) unless using retina display which are often (2,2)
|
||||||
|
|
||||||
|
// Render command lists
|
||||||
|
size_t vertexBufferOffset = 0;
|
||||||
|
size_t indexBufferOffset = 0;
|
||||||
|
for (int n = 0; n < draw_data->CmdListsCount; n++)
|
||||||
|
{
|
||||||
|
const ImDrawList* draw_list = draw_data->CmdLists[n];
|
||||||
|
|
||||||
|
memcpy((char*)vertexBuffer.buffer.contents + vertexBufferOffset, draw_list->VtxBuffer.Data, (size_t)draw_list->VtxBuffer.Size * sizeof(ImDrawVert));
|
||||||
|
memcpy((char*)indexBuffer.buffer.contents + indexBufferOffset, draw_list->IdxBuffer.Data, (size_t)draw_list->IdxBuffer.Size * sizeof(ImDrawIdx));
|
||||||
|
|
||||||
|
for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++)
|
||||||
|
{
|
||||||
|
const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i];
|
||||||
|
if (pcmd->UserCallback)
|
||||||
|
{
|
||||||
|
// User callback, registered via ImDrawList::AddCallback()
|
||||||
|
// (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.)
|
||||||
|
if (pcmd->UserCallback == ImDrawCallback_ResetRenderState)
|
||||||
|
ImGui_ImplMetal_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, vertexBufferOffset);
|
||||||
|
else
|
||||||
|
pcmd->UserCallback(draw_list, pcmd);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Project scissor/clipping rectangles into framebuffer space
|
||||||
|
ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y);
|
||||||
|
ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y);
|
||||||
|
|
||||||
|
// Clamp to viewport as setScissorRect() won't accept values that are off bounds
|
||||||
|
if (clip_min.x < 0.0f) { clip_min.x = 0.0f; }
|
||||||
|
if (clip_min.y < 0.0f) { clip_min.y = 0.0f; }
|
||||||
|
if (clip_max.x > fb_width) { clip_max.x = (float)fb_width; }
|
||||||
|
if (clip_max.y > fb_height) { clip_max.y = (float)fb_height; }
|
||||||
|
if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
|
||||||
|
continue;
|
||||||
|
if (pcmd->ElemCount == 0) // drawIndexedPrimitives() validation doesn't accept this
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Apply scissor/clipping rectangle
|
||||||
|
MTLScissorRect scissorRect =
|
||||||
|
{
|
||||||
|
.x = NSUInteger(clip_min.x),
|
||||||
|
.y = NSUInteger(clip_min.y),
|
||||||
|
.width = NSUInteger(clip_max.x - clip_min.x),
|
||||||
|
.height = NSUInteger(clip_max.y - clip_min.y)
|
||||||
|
};
|
||||||
|
[commandEncoder setScissorRect:scissorRect];
|
||||||
|
|
||||||
|
// Bind texture, Draw
|
||||||
|
if (ImTextureID tex_id = pcmd->GetTexID())
|
||||||
|
[commandEncoder setFragmentTexture:(__bridge id<MTLTexture>)(void*)(intptr_t)(tex_id) atIndex:0];
|
||||||
|
|
||||||
|
[commandEncoder setVertexBufferOffset:(vertexBufferOffset + pcmd->VtxOffset * sizeof(ImDrawVert)) atIndex:0];
|
||||||
|
[commandEncoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle
|
||||||
|
indexCount:pcmd->ElemCount
|
||||||
|
indexType:sizeof(ImDrawIdx) == 2 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32
|
||||||
|
indexBuffer:indexBuffer.buffer
|
||||||
|
indexBufferOffset:indexBufferOffset + pcmd->IdxOffset * sizeof(ImDrawIdx)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vertexBufferOffset += (size_t)draw_list->VtxBuffer.Size * sizeof(ImDrawVert);
|
||||||
|
indexBufferOffset += (size_t)draw_list->IdxBuffer.Size * sizeof(ImDrawIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
MetalContext* sharedMetalContext = bd->SharedMetalContext;
|
||||||
|
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer>)
|
||||||
|
{
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
@synchronized(sharedMetalContext.bufferCache)
|
||||||
|
{
|
||||||
|
[sharedMetalContext.bufferCache addObject:vertexBuffer];
|
||||||
|
[sharedMetalContext.bufferCache addObject:indexBuffer];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ImGui_ImplMetal_DestroyTexture(ImTextureData* tex)
|
||||||
|
{
|
||||||
|
MetalTexture* backend_tex = (__bridge_transfer MetalTexture*)(tex->BackendUserData);
|
||||||
|
if (backend_tex == nullptr)
|
||||||
|
return;
|
||||||
|
IM_ASSERT(backend_tex.metalTexture == (__bridge id<MTLTexture>)(void*)(intptr_t)tex->TexID);
|
||||||
|
backend_tex.metalTexture = nil;
|
||||||
|
|
||||||
|
// Clear identifiers and mark as destroyed (in order to allow e.g. calling InvalidateDeviceObjects while running)
|
||||||
|
tex->SetTexID(ImTextureID_Invalid);
|
||||||
|
tex->SetStatus(ImTextureStatus_Destroyed);
|
||||||
|
tex->BackendUserData = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex)
|
||||||
|
{
|
||||||
|
ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
|
||||||
|
if (tex->Status == ImTextureStatus_WantCreate)
|
||||||
|
{
|
||||||
|
// Create and upload new texture to graphics system
|
||||||
|
//IMGUI_DEBUG_LOG("UpdateTexture #%03d: WantCreate %dx%d\n", tex->UniqueID, tex->Width, tex->Height);
|
||||||
|
IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == nullptr);
|
||||||
|
IM_ASSERT(tex->Format == ImTextureFormat_RGBA32);
|
||||||
|
|
||||||
|
// We are retrieving and uploading the font atlas as a 4-channels RGBA texture here.
|
||||||
|
// In theory we could call GetTexDataAsAlpha8() and upload a 1-channel texture to save on memory access bandwidth.
|
||||||
|
// However, using a shader designed for 1-channel texture would make it less obvious to use the ImTextureID facility to render users own textures.
|
||||||
|
// You can make that change in your implementation.
|
||||||
|
MTLTextureDescriptor* textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
|
||||||
|
width:(NSUInteger)tex->Width
|
||||||
|
height:(NSUInteger)tex->Height
|
||||||
|
mipmapped:NO];
|
||||||
|
textureDescriptor.usage = MTLTextureUsageShaderRead;
|
||||||
|
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
|
||||||
|
textureDescriptor.storageMode = MTLStorageModeManaged;
|
||||||
|
#else
|
||||||
|
textureDescriptor.storageMode = MTLStorageModeShared;
|
||||||
|
#endif
|
||||||
|
id <MTLTexture> texture = [bd->SharedMetalContext.device newTextureWithDescriptor:textureDescriptor];
|
||||||
|
[texture replaceRegion:MTLRegionMake2D(0, 0, (NSUInteger)tex->Width, (NSUInteger)tex->Height) mipmapLevel:0 withBytes:tex->Pixels bytesPerRow:(NSUInteger)tex->Width * 4];
|
||||||
|
MetalTexture* backend_tex = [[MetalTexture alloc] initWithTexture:texture];
|
||||||
|
|
||||||
|
// Store identifiers
|
||||||
|
tex->SetTexID((ImTextureID)(intptr_t)texture);
|
||||||
|
tex->SetStatus(ImTextureStatus_OK);
|
||||||
|
tex->BackendUserData = (__bridge_retained void*)(backend_tex);
|
||||||
|
}
|
||||||
|
else if (tex->Status == ImTextureStatus_WantUpdates)
|
||||||
|
{
|
||||||
|
// Update selected blocks. We only ever write to textures regions which have never been used before!
|
||||||
|
// This backend choose to use tex->Updates[] but you can use tex->UpdateRect to upload a single region.
|
||||||
|
MetalTexture* backend_tex = (__bridge MetalTexture*)(tex->BackendUserData);
|
||||||
|
for (ImTextureRect& r : tex->Updates)
|
||||||
|
{
|
||||||
|
[backend_tex.metalTexture replaceRegion:MTLRegionMake2D((NSUInteger)r.x, (NSUInteger)r.y, (NSUInteger)r.w, (NSUInteger)r.h)
|
||||||
|
mipmapLevel:0
|
||||||
|
withBytes:tex->GetPixelsAt(r.x, r.y)
|
||||||
|
bytesPerRow:(NSUInteger)tex->Width * 4];
|
||||||
|
}
|
||||||
|
tex->SetStatus(ImTextureStatus_OK);
|
||||||
|
}
|
||||||
|
else if (tex->Status == ImTextureStatus_WantDestroy && tex->UnusedFrames > 0)
|
||||||
|
{
|
||||||
|
ImGui_ImplMetal_DestroyTexture(tex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImGui_ImplMetal_CreateDeviceObjects(id<MTLDevice> device)
|
||||||
|
{
|
||||||
|
ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
|
||||||
|
MTLDepthStencilDescriptor* depthStencilDescriptor = [[MTLDepthStencilDescriptor alloc] init];
|
||||||
|
depthStencilDescriptor.depthWriteEnabled = NO;
|
||||||
|
depthStencilDescriptor.depthCompareFunction = MTLCompareFunctionAlways;
|
||||||
|
bd->SharedMetalContext.depthStencilState = [device newDepthStencilStateWithDescriptor:depthStencilDescriptor];
|
||||||
|
#ifdef IMGUI_IMPL_METAL_CPP
|
||||||
|
[depthStencilDescriptor release];
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGui_ImplMetal_DestroyDeviceObjects()
|
||||||
|
{
|
||||||
|
ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
|
||||||
|
|
||||||
|
// Destroy all textures
|
||||||
|
for (ImTextureData* tex : ImGui::GetPlatformIO().Textures)
|
||||||
|
if (tex->RefCount == 1)
|
||||||
|
ImGui_ImplMetal_DestroyTexture(tex);
|
||||||
|
|
||||||
|
[bd->SharedMetalContext.renderPipelineStateCache removeAllObjects];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - MetalBuffer implementation
|
||||||
|
|
||||||
|
@implementation MetalBuffer
|
||||||
|
- (instancetype)initWithBuffer:(id<MTLBuffer>)buffer
|
||||||
|
{
|
||||||
|
if ((self = [super init]))
|
||||||
|
{
|
||||||
|
_buffer = buffer;
|
||||||
|
_lastReuseTime = GetMachAbsoluteTimeInSeconds();
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
#pragma mark - FramebufferDescriptor implementation
|
||||||
|
|
||||||
|
@implementation FramebufferDescriptor
|
||||||
|
- (instancetype)initWithRenderPassDescriptor:(MTLRenderPassDescriptor*)renderPassDescriptor
|
||||||
|
{
|
||||||
|
if ((self = [super init]))
|
||||||
|
{
|
||||||
|
_sampleCount = renderPassDescriptor.colorAttachments[0].texture.sampleCount;
|
||||||
|
_colorPixelFormat = renderPassDescriptor.colorAttachments[0].texture.pixelFormat;
|
||||||
|
_depthPixelFormat = renderPassDescriptor.depthAttachment.texture.pixelFormat;
|
||||||
|
_stencilPixelFormat = renderPassDescriptor.stencilAttachment.texture.pixelFormat;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (nonnull id)copyWithZone:(nullable NSZone*)zone
|
||||||
|
{
|
||||||
|
FramebufferDescriptor* copy = [[FramebufferDescriptor allocWithZone:zone] init];
|
||||||
|
copy.sampleCount = self.sampleCount;
|
||||||
|
copy.colorPixelFormat = self.colorPixelFormat;
|
||||||
|
copy.depthPixelFormat = self.depthPixelFormat;
|
||||||
|
copy.stencilPixelFormat = self.stencilPixelFormat;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSUInteger)hash
|
||||||
|
{
|
||||||
|
NSUInteger sc = _sampleCount & 0x3;
|
||||||
|
NSUInteger cf = _colorPixelFormat & 0x3FF;
|
||||||
|
NSUInteger df = _depthPixelFormat & 0x3FF;
|
||||||
|
NSUInteger sf = _stencilPixelFormat & 0x3FF;
|
||||||
|
NSUInteger hash = (sf << 22) | (df << 12) | (cf << 2) | sc;
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isEqual:(id)object
|
||||||
|
{
|
||||||
|
FramebufferDescriptor* other = object;
|
||||||
|
if (![other isKindOfClass:[FramebufferDescriptor class]])
|
||||||
|
return NO;
|
||||||
|
return other.sampleCount == self.sampleCount &&
|
||||||
|
other.colorPixelFormat == self.colorPixelFormat &&
|
||||||
|
other.depthPixelFormat == self.depthPixelFormat &&
|
||||||
|
other.stencilPixelFormat == self.stencilPixelFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
#pragma mark - MetalTexture implementation
|
||||||
|
|
||||||
|
@implementation MetalTexture
|
||||||
|
- (instancetype)initWithTexture:(id<MTLTexture>)metalTexture
|
||||||
|
{
|
||||||
|
if ((self = [super init]))
|
||||||
|
self.metalTexture = metalTexture;
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
#pragma mark - MetalContext implementation
|
||||||
|
|
||||||
|
@implementation MetalContext
|
||||||
|
- (instancetype)init
|
||||||
|
{
|
||||||
|
if ((self = [super init]))
|
||||||
|
{
|
||||||
|
self.renderPipelineStateCache = [NSMutableDictionary dictionary];
|
||||||
|
self.bufferCache = [NSMutableArray array];
|
||||||
|
_lastBufferCachePurge = GetMachAbsoluteTimeInSeconds();
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTLDevice>)device
|
||||||
|
{
|
||||||
|
uint64_t now = GetMachAbsoluteTimeInSeconds();
|
||||||
|
|
||||||
|
@synchronized(self.bufferCache)
|
||||||
|
{
|
||||||
|
// Purge old buffers that haven't been useful for a while
|
||||||
|
if (now - self.lastBufferCachePurge > 1.0)
|
||||||
|
{
|
||||||
|
NSMutableArray* survivors = [NSMutableArray array];
|
||||||
|
for (MetalBuffer* candidate in self.bufferCache)
|
||||||
|
if (candidate.lastReuseTime > self.lastBufferCachePurge)
|
||||||
|
[survivors addObject:candidate];
|
||||||
|
self.bufferCache = [survivors mutableCopy];
|
||||||
|
self.lastBufferCachePurge = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// See if we have a buffer we can reuse
|
||||||
|
MetalBuffer* bestCandidate = nil;
|
||||||
|
for (MetalBuffer* candidate in self.bufferCache)
|
||||||
|
if (candidate.buffer.length >= length && (bestCandidate == nil || bestCandidate.lastReuseTime > candidate.lastReuseTime))
|
||||||
|
bestCandidate = candidate;
|
||||||
|
|
||||||
|
if (bestCandidate != nil)
|
||||||
|
{
|
||||||
|
[self.bufferCache removeObject:bestCandidate];
|
||||||
|
bestCandidate.lastReuseTime = now;
|
||||||
|
return bestCandidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No luck; make a new buffer
|
||||||
|
id<MTLBuffer> backing = [device newBufferWithLength:length options:MTLResourceStorageModeShared];
|
||||||
|
return [[MetalBuffer alloc] initWithBuffer:backing];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling.
|
||||||
|
- (id<MTLRenderPipelineState>)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id<MTLDevice>)device
|
||||||
|
{
|
||||||
|
NSError* error = nil;
|
||||||
|
|
||||||
|
NSString* shaderSource = @""
|
||||||
|
"#include <metal_stdlib>\n"
|
||||||
|
"using namespace metal;\n"
|
||||||
|
"\n"
|
||||||
|
"struct Uniforms {\n"
|
||||||
|
" float4x4 projectionMatrix;\n"
|
||||||
|
"};\n"
|
||||||
|
"\n"
|
||||||
|
"struct VertexIn {\n"
|
||||||
|
" float2 position [[attribute(0)]];\n"
|
||||||
|
" float2 texCoords [[attribute(1)]];\n"
|
||||||
|
" uchar4 color [[attribute(2)]];\n"
|
||||||
|
"};\n"
|
||||||
|
"\n"
|
||||||
|
"struct VertexOut {\n"
|
||||||
|
" float4 position [[position]];\n"
|
||||||
|
" float2 texCoords;\n"
|
||||||
|
" float4 color;\n"
|
||||||
|
"};\n"
|
||||||
|
"\n"
|
||||||
|
"vertex VertexOut vertex_main(VertexIn in [[stage_in]],\n"
|
||||||
|
" constant Uniforms &uniforms [[buffer(1)]]) {\n"
|
||||||
|
" VertexOut out;\n"
|
||||||
|
" out.position = uniforms.projectionMatrix * float4(in.position, 0, 1);\n"
|
||||||
|
" out.texCoords = in.texCoords;\n"
|
||||||
|
" out.color = float4(in.color) / float4(255.0);\n"
|
||||||
|
" return out;\n"
|
||||||
|
"}\n"
|
||||||
|
"\n"
|
||||||
|
"fragment half4 fragment_main(VertexOut in [[stage_in]],\n"
|
||||||
|
" texture2d<half, access::sample> texture [[texture(0)]]) {\n"
|
||||||
|
" constexpr sampler linearSampler(coord::normalized, min_filter::linear, mag_filter::linear, mip_filter::linear);\n"
|
||||||
|
" half4 texColor = texture.sample(linearSampler, in.texCoords);\n"
|
||||||
|
" return half4(in.color) * texColor;\n"
|
||||||
|
"}\n";
|
||||||
|
|
||||||
|
id<MTLLibrary> library = [device newLibraryWithSource:shaderSource options:nil error:&error];
|
||||||
|
if (library == nil)
|
||||||
|
{
|
||||||
|
NSLog(@"Error: failed to create Metal library: %@", error);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
id<MTLFunction> vertexFunction = [library newFunctionWithName:@"vertex_main"];
|
||||||
|
id<MTLFunction> fragmentFunction = [library newFunctionWithName:@"fragment_main"];
|
||||||
|
|
||||||
|
if (vertexFunction == nil || fragmentFunction == nil)
|
||||||
|
{
|
||||||
|
NSLog(@"Error: failed to find Metal shader functions in library: %@", error);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
MTLVertexDescriptor* vertexDescriptor = [MTLVertexDescriptor vertexDescriptor];
|
||||||
|
vertexDescriptor.attributes[0].offset = offsetof(ImDrawVert, pos);
|
||||||
|
vertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; // position
|
||||||
|
vertexDescriptor.attributes[0].bufferIndex = 0;
|
||||||
|
vertexDescriptor.attributes[1].offset = offsetof(ImDrawVert, uv);
|
||||||
|
vertexDescriptor.attributes[1].format = MTLVertexFormatFloat2; // texCoords
|
||||||
|
vertexDescriptor.attributes[1].bufferIndex = 0;
|
||||||
|
vertexDescriptor.attributes[2].offset = offsetof(ImDrawVert, col);
|
||||||
|
vertexDescriptor.attributes[2].format = MTLVertexFormatUChar4; // color
|
||||||
|
vertexDescriptor.attributes[2].bufferIndex = 0;
|
||||||
|
vertexDescriptor.layouts[0].stepRate = 1;
|
||||||
|
vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
|
||||||
|
vertexDescriptor.layouts[0].stride = sizeof(ImDrawVert);
|
||||||
|
|
||||||
|
MTLRenderPipelineDescriptor* pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
|
||||||
|
pipelineDescriptor.vertexFunction = vertexFunction;
|
||||||
|
pipelineDescriptor.fragmentFunction = fragmentFunction;
|
||||||
|
pipelineDescriptor.vertexDescriptor = vertexDescriptor;
|
||||||
|
pipelineDescriptor.rasterSampleCount = self.framebufferDescriptor.sampleCount;
|
||||||
|
pipelineDescriptor.colorAttachments[0].pixelFormat = self.framebufferDescriptor.colorPixelFormat;
|
||||||
|
pipelineDescriptor.colorAttachments[0].blendingEnabled = YES;
|
||||||
|
pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
|
||||||
|
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
|
||||||
|
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
|
||||||
|
pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
|
||||||
|
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne;
|
||||||
|
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
|
||||||
|
pipelineDescriptor.depthAttachmentPixelFormat = self.framebufferDescriptor.depthPixelFormat;
|
||||||
|
pipelineDescriptor.stencilAttachmentPixelFormat = self.framebufferDescriptor.stencilPixelFormat;
|
||||||
|
|
||||||
|
id<MTLRenderPipelineState> renderPipelineState = [device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error];
|
||||||
|
if (error != nil)
|
||||||
|
NSLog(@"Error: failed to create Metal pipeline state: %@", error);
|
||||||
|
|
||||||
|
return renderPipelineState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#endif // #ifndef IMGUI_DISABLE
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="24506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="landscape" appearance="light"/>
|
<device id="retina6_1" orientation="landscape" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24504"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@@ -12,8 +13,13 @@
|
|||||||
<view contentMode="scaleToFill" id="VWy-YH-qYj">
|
<view contentMode="scaleToFill" id="VWy-YH-qYj">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
|
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<color key="backgroundColor" white="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
<point key="canvasLocation" x="33" y="187"/>
|
<point key="canvasLocation" x="33" y="187"/>
|
||||||
</view>
|
</view>
|
||||||
</objects>
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<systemColor name="systemBackgroundColor">
|
||||||
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES" initialViewController="EVd-wq-ego">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES" initialViewController="EVd-wq-ego">
|
||||||
<device id="ipad11_0rounded" orientation="portrait" layout="fullscreen" appearance="light"/>
|
<device id="ipad11_0rounded" orientation="portrait" layout="fullscreen" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<scenes>
|
<scenes>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<objects>
|
<objects>
|
||||||
<viewController id="wb7-af-jn8" customClass="MainFrameViewController" sceneMemberID="viewController">
|
<viewController id="wb7-af-jn8" customClass="MainFrameViewController" sceneMemberID="viewController">
|
||||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" indicatorStyle="black" dataMode="prototypes" id="TZj-Lc-M9d" customClass="AppCollectionView">
|
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" indicatorStyle="black" dataMode="prototypes" id="TZj-Lc-M9d" customClass="AppCollectionView">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="834" height="1120"/>
|
<rect key="frame" x="0.0" y="0.0" width="834" height="1136"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<color key="backgroundColor" red="0.3333333432674408" green="0.3333333432674408" blue="0.3333333432674408" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="backgroundColor" red="0.3333333432674408" green="0.3333333432674408" blue="0.3333333432674408" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="20" id="f7l-kG-hJc">
|
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="20" id="f7l-kG-hJc">
|
||||||
@@ -601,6 +601,25 @@ ICAgICAgICAgICAgICA
|
|||||||
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</segmentedControl>
|
</segmentedControl>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Player 1 Motion Mode" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0bl-Hh-8ot">
|
||||||
|
<rect key="frame" x="20" y="2154" width="164" height="21"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="WQX-ct-rnt" userLabel="Motion Mode Selector">
|
||||||
|
<rect key="frame" x="16" y="2183" width="459" height="32"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<segments>
|
||||||
|
<segment title="Auto"/>
|
||||||
|
<segment title="Device Motion"/>
|
||||||
|
<segment title="Controller Motion"/>
|
||||||
|
</segments>
|
||||||
|
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</segmentedControl>
|
||||||
</subviews>
|
</subviews>
|
||||||
<color key="backgroundColor" systemColor="viewFlipsideBackgroundColor"/>
|
<color key="backgroundColor" systemColor="viewFlipsideBackgroundColor"/>
|
||||||
</view>
|
</view>
|
||||||
@@ -622,6 +641,7 @@ ICAgICAgICAgICAgICA
|
|||||||
<outlet property="largerStickLR1Selector" destination="sIU-PH-H7V" id="YWg-40-eKd"/>
|
<outlet property="largerStickLR1Selector" destination="sIU-PH-H7V" id="YWg-40-eKd"/>
|
||||||
<outlet property="liftStreamViewForKeyboardSelector" destination="hm3-EV-h6d" id="fHG-J1-eQL"/>
|
<outlet property="liftStreamViewForKeyboardSelector" destination="hm3-EV-h6d" id="fHG-J1-eQL"/>
|
||||||
<outlet property="localMousePointerModeSelector" destination="4FU-iP-c6T" id="4Gz-ao-xH2"/>
|
<outlet property="localMousePointerModeSelector" destination="4FU-iP-c6T" id="4Gz-ao-xH2"/>
|
||||||
|
<outlet property="motionModeSelector" destination="WQX-ct-rnt" id="1N8-yA-54x"/>
|
||||||
<outlet property="mousePointerVelocityFactorSlider" destination="Ei2-FJ-9oC" id="1T3-QA-Jua"/>
|
<outlet property="mousePointerVelocityFactorSlider" destination="Ei2-FJ-9oC" id="1T3-QA-Jua"/>
|
||||||
<outlet property="mousePointerVelocityFactorUILabel" destination="oPM-G6-u5O" id="Eco-5u-PRR"/>
|
<outlet property="mousePointerVelocityFactorUILabel" destination="oPM-G6-u5O" id="Eco-5u-PRR"/>
|
||||||
<outlet property="multiControllerSelector" destination="KlC-fG-wEi" id="giM-F7-So5"/>
|
<outlet property="multiControllerSelector" destination="KlC-fG-wEi" id="giM-F7-So5"/>
|
||||||
@@ -664,7 +684,7 @@ ICAgICAgICAgICAgICA
|
|||||||
<viewControllerLayoutGuide type="bottom" id="YRO-Lg-RCg"/>
|
<viewControllerLayoutGuide type="bottom" id="YRO-Lg-RCg"/>
|
||||||
</layoutGuides>
|
</layoutGuides>
|
||||||
<view key="view" contentMode="scaleToFill" id="hGs-BR-t4b">
|
<view key="view" contentMode="scaleToFill" id="hGs-BR-t4b">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="834" height="1194"/>
|
<rect key="frame" x="0.0" y="0.0" width="834" height="1210"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</view>
|
</view>
|
||||||
@@ -704,7 +724,7 @@ ICAgICAgICAgICAgICA
|
|||||||
<viewControllerLayoutGuide type="bottom" id="vu8-3s-HlY"/>
|
<viewControllerLayoutGuide type="bottom" id="vu8-3s-HlY"/>
|
||||||
</layoutGuides>
|
</layoutGuides>
|
||||||
<view key="view" multipleTouchEnabled="YES" contentMode="scaleToFill" id="SMn-1j-R3I">
|
<view key="view" multipleTouchEnabled="YES" contentMode="scaleToFill" id="SMn-1j-R3I">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="834" height="1194"/>
|
<rect key="frame" x="0.0" y="0.0" width="834" height="1210"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<color key="backgroundColor" red="0.3333357871" green="0.33332890269999998" blue="0.33333355190000002" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
<color key="backgroundColor" red="0.3333357871" green="0.33332890269999998" blue="0.33333355190000002" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||||
</view>
|
</view>
|
||||||
@@ -726,7 +746,7 @@ ICAgICAgICAgICAgICA
|
|||||||
<viewControllerLayoutGuide type="bottom" id="YZj-An-sqS"/>
|
<viewControllerLayoutGuide type="bottom" id="YZj-An-sqS"/>
|
||||||
</layoutGuides>
|
</layoutGuides>
|
||||||
<view key="view" contentMode="scaleToFill" id="le1-tU-NEp">
|
<view key="view" contentMode="scaleToFill" id="le1-tU-NEp">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="834" height="1194"/>
|
<rect key="frame" x="0.0" y="0.0" width="834" height="1210"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" animating="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="N0D-dj-EuF">
|
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" animating="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="N0D-dj-EuF">
|
||||||
@@ -753,7 +773,7 @@ ICAgICAgICAgICAgICA
|
|||||||
<viewControllerLayoutGuide type="bottom" id="eaS-aj-VtA"/>
|
<viewControllerLayoutGuide type="bottom" id="eaS-aj-VtA"/>
|
||||||
</layoutGuides>
|
</layoutGuides>
|
||||||
<view key="view" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="Acw-S4-f8a">
|
<view key="view" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="Acw-S4-f8a">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="834" height="1194"/>
|
<rect key="frame" x="0.0" y="0.0" width="834" height="1210"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<view alpha="0.75" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fZU-G0-fgM" userLabel="Toolbar Root View" customClass="ToolBarContainerView">
|
<view alpha="0.75" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fZU-G0-fgM" userLabel="Toolbar Root View" customClass="ToolBarContainerView">
|
||||||
@@ -965,17 +985,17 @@ ICAgICAgICAgICAgICA
|
|||||||
<!--Profiles Table View Controller-->
|
<!--Profiles Table View Controller-->
|
||||||
<scene sceneID="W0E-wz-n2b">
|
<scene sceneID="W0E-wz-n2b">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="OSCProfilesTableViewController" id="Atv-Nj-0qv" customClass="OSCProfilesTableViewController" sceneMemberID="viewController">
|
<viewController storyboardIdentifier="OSCProfilesTableViewController" id="Atv-Nj-0qv" customClass="OSCProfilesTableViewController" customModule="VoidLink" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<layoutGuides>
|
<layoutGuides>
|
||||||
<viewControllerLayoutGuide type="top" id="osA-Uy-SE1"/>
|
<viewControllerLayoutGuide type="top" id="osA-Uy-SE1"/>
|
||||||
<viewControllerLayoutGuide type="bottom" id="8ja-wQ-jbJ"/>
|
<viewControllerLayoutGuide type="bottom" id="8ja-wQ-jbJ"/>
|
||||||
</layoutGuides>
|
</layoutGuides>
|
||||||
<view key="view" contentMode="scaleToFill" id="PlJ-LJ-d1p">
|
<view key="view" contentMode="scaleToFill" id="PlJ-LJ-d1p">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="834" height="1194"/>
|
<rect key="frame" x="0.0" y="0.0" width="834" height="1210"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<tableView opaque="NO" clearsContextBeforeDrawing="NO" alpha="0.5" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="-1" estimatedSectionHeaderHeight="-1" sectionFooterHeight="-1" estimatedSectionFooterHeight="-1" translatesAutoresizingMaskIntoConstraints="NO" id="BA3-hd-bDp">
|
<tableView opaque="NO" clearsContextBeforeDrawing="NO" alpha="0.5" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="-1" estimatedSectionHeaderHeight="-1" sectionFooterHeight="-1" estimatedSectionFooterHeight="-1" translatesAutoresizingMaskIntoConstraints="NO" id="BA3-hd-bDp">
|
||||||
<rect key="frame" x="0.0" y="44" width="834" height="1150"/>
|
<rect key="frame" x="0.0" y="44" width="834" height="1166"/>
|
||||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
</tableView>
|
</tableView>
|
||||||
<navigationBar contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="62V-DN-FUk">
|
<navigationBar contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="62V-DN-FUk">
|
||||||
@@ -1036,13 +1056,13 @@ ICAgICAgICAgICAgICA
|
|||||||
<image name="SquaresArrowUp" width="40" height="40"/>
|
<image name="SquaresArrowUp" width="40" height="40"/>
|
||||||
<image name="Trash" width="40" height="40"/>
|
<image name="Trash" width="40" height="40"/>
|
||||||
<systemColor name="secondaryLabelColor">
|
<systemColor name="secondaryLabelColor">
|
||||||
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
<systemColor name="systemBackgroundColor">
|
<systemColor name="systemBackgroundColor">
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
<systemColor name="tintColor">
|
<systemColor name="tintColor">
|
||||||
<color red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
<systemColor name="viewFlipsideBackgroundColor">
|
<systemColor name="viewFlipsideBackgroundColor">
|
||||||
<color red="0.12156862745098039" green="0.12941176470588237" blue="0.14117647058823529" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.12156862745098039" green="0.12941176470588237" blue="0.14117647058823529" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES" initialViewController="DL0-L5-LOv">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES" initialViewController="DL0-L5-LOv">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
@@ -570,7 +570,7 @@ ICAgICAgICAgICAgIA
|
|||||||
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</segmentedControl>
|
</segmentedControl>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Local Mouse Pointer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VGn-Oc-mKc">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Local Mouse Pointer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VGn-Oc-mKc">
|
||||||
<rect key="frame" x="22" y="2007" width="165" height="21"/>
|
<rect key="frame" x="20" y="2008" width="165" height="21"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
@@ -604,6 +604,24 @@ ICAgICAgICAgICAgIA
|
|||||||
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Player 1 Motion Mode" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c8T-vY-LOX">
|
||||||
|
<rect key="frame" x="22" y="2075" width="174" height="21"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
|
<color key="textColor" red="0.95132106540000005" green="0.97490358349999995" blue="0.99987185000000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="6Lj-ss-Tj8" userLabel="Motion Mode Selector">
|
||||||
|
<rect key="frame" x="16" y="2104" width="450" height="32"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<segments>
|
||||||
|
<segment title="Auto"/>
|
||||||
|
<segment title="Device Motion"/>
|
||||||
|
<segment title="Controller Motion"/>
|
||||||
|
</segments>
|
||||||
|
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</segmentedControl>
|
||||||
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="FqF-bM-afQ">
|
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="FqF-bM-afQ">
|
||||||
<rect key="frame" x="17" y="14" width="219" height="32"/>
|
<rect key="frame" x="17" y="14" width="219" height="32"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
@@ -617,6 +635,23 @@ ICAgICAgICAgICAgIA
|
|||||||
<action selector="backToStreamingButtonTapped:" destination="rYd-e6-cQU" eventType="touchUpInside" id="9P0-Yw-T4W"/>
|
<action selector="backToStreamingButtonTapped:" destination="rYd-e6-cQU" eventType="touchUpInside" id="9P0-Yw-T4W"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
|
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="dcP-yB-6Un" userLabel="PiP Selector">
|
||||||
|
<rect key="frame" x="18" y="2102" width="459" height="28"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<segments>
|
||||||
|
<segment title="No"/>
|
||||||
|
<segment title="Yes"/>
|
||||||
|
</segments>
|
||||||
|
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</segmentedControl>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Enable PiP" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ELc-8r-KSB">
|
||||||
|
<rect key="frame" x="20" y="2075" width="86" height="19"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
|
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
</subviews>
|
</subviews>
|
||||||
<color key="backgroundColor" red="0.12156862745098039" green="0.12941176470588237" blue="0.14117647058823529" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="backgroundColor" red="0.12156862745098039" green="0.12941176470588237" blue="0.14117647058823529" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</view>
|
</view>
|
||||||
@@ -646,6 +681,7 @@ ICAgICAgICAgICAgIA
|
|||||||
<outlet property="optimizeSettingsSelector" destination="Gob-Lu-b1y" id="9Wl-yp-0Ou"/>
|
<outlet property="optimizeSettingsSelector" destination="Gob-Lu-b1y" id="9Wl-yp-0Ou"/>
|
||||||
<outlet property="oscTapExlusionAreaSizeLabel" destination="Dfl-gL-00M" id="Jbg-64-P3M"/>
|
<outlet property="oscTapExlusionAreaSizeLabel" destination="Dfl-gL-00M" id="Jbg-64-P3M"/>
|
||||||
<outlet property="oscTapExlusionAreaSizeSlider" destination="TSt-Hj-uTR" id="h9l-ap-AVd"/>
|
<outlet property="oscTapExlusionAreaSizeSlider" destination="TSt-Hj-uTR" id="h9l-ap-AVd"/>
|
||||||
|
<outlet property="pipSelector" destination="dcP-yB-6Un" id="rrB-dR-2Bq"/>
|
||||||
<outlet property="pointerVelocityModeDividerSlider" destination="xet-C9-l5y" id="GnQ-xU-e0N"/>
|
<outlet property="pointerVelocityModeDividerSlider" destination="xet-C9-l5y" id="GnQ-xU-e0N"/>
|
||||||
<outlet property="pointerVelocityModeDividerUILabel" destination="VlN-ZW-1gm" id="vbe-QK-hZb"/>
|
<outlet property="pointerVelocityModeDividerUILabel" destination="VlN-ZW-1gm" id="vbe-QK-hZb"/>
|
||||||
<outlet property="resolutionDisplayView" destination="Dej-Bj-F6i" id="bi4-N6-snx"/>
|
<outlet property="resolutionDisplayView" destination="Dej-Bj-F6i" id="bi4-N6-snx"/>
|
||||||
@@ -665,6 +701,7 @@ ICAgICAgICAgICAgIA
|
|||||||
<outlet property="touchPointerVelocityFactorUILabel" destination="c6f-bJ-Vji" id="uVM-o6-JPk"/>
|
<outlet property="touchPointerVelocityFactorUILabel" destination="c6f-bJ-Vji" id="uVM-o6-JPk"/>
|
||||||
<outlet property="unlockDisplayOrientationSelector" destination="R9O-Tw-UD4" id="0ak-pc-eva"/>
|
<outlet property="unlockDisplayOrientationSelector" destination="R9O-Tw-UD4" id="0ak-pc-eva"/>
|
||||||
<outlet property="yuv444Selector" destination="vwT-tg-RfT" id="69G-bN-kJF"/>
|
<outlet property="yuv444Selector" destination="vwT-tg-RfT" id="69G-bN-kJF"/>
|
||||||
|
<outlet property="motionModeSelector" destination="6Lj-ss-Tj8" id="RLW-3I-hvl"/>
|
||||||
</connections>
|
</connections>
|
||||||
</viewController>
|
</viewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="RWc-Km-KG5" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="RWc-Km-KG5" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
@@ -968,7 +1005,7 @@ ICAgICAgICAgICAgIA
|
|||||||
<!--Profiles Table View Controller-->
|
<!--Profiles Table View Controller-->
|
||||||
<scene sceneID="C7c-yg-gQK">
|
<scene sceneID="C7c-yg-gQK">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController storyboardIdentifier="OSCProfilesTableViewController" id="0K8-kQ-xlN" customClass="OSCProfilesTableViewController" sceneMemberID="viewController">
|
<viewController storyboardIdentifier="OSCProfilesTableViewController" id="0K8-kQ-xlN" customClass="OSCProfilesTableViewController" customModule="VoidLink" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<layoutGuides>
|
<layoutGuides>
|
||||||
<viewControllerLayoutGuide type="top" id="Nat-X1-41w"/>
|
<viewControllerLayoutGuide type="top" id="Nat-X1-41w"/>
|
||||||
<viewControllerLayoutGuide type="bottom" id="9SV-fx-82v"/>
|
<viewControllerLayoutGuide type="bottom" id="9SV-fx-82v"/>
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
//
|
|
||||||
// ComputerScrollView.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 9/30/15.
|
|
||||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
@interface ComputerScrollView : UIScrollView
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
//
|
|
||||||
// ComputerScrollView.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 9/30/15.
|
|
||||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ComputerScrollView.h"
|
|
||||||
|
|
||||||
@implementation ComputerScrollView
|
|
||||||
|
|
||||||
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
|
|
||||||
if ([view isKindOfClass:[UIButton class]]) {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
return [super touchesShouldCancelInContentView:view];
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
// Only override drawRect: if you perform custom drawing.
|
|
||||||
// An empty implementation adversely affects performance during animation.
|
|
||||||
- (void)drawRect:(CGRect)rect {
|
|
||||||
// Drawing code
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
//
|
|
||||||
// DataManager.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 10/28/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "AppDelegate.h"
|
|
||||||
#import "TemporaryHost.h"
|
|
||||||
#import "TemporaryApp.h"
|
|
||||||
#import "TemporarySettings.h"
|
|
||||||
|
|
||||||
@interface DataManager : NSObject
|
|
||||||
|
|
||||||
- (void) saveSettingsWithBitrate:(NSInteger)bitrate
|
|
||||||
framerate:(NSInteger)framerate
|
|
||||||
height:(NSInteger)height
|
|
||||||
width:(NSInteger)width
|
|
||||||
audioConfig:(NSInteger)audioConfig
|
|
||||||
onscreenControls:(NSInteger)onscreenControls
|
|
||||||
keyboardToggleFingers:(NSInteger)keyboardToggleFingers
|
|
||||||
oscLayoutToolFingers:(NSInteger)oscLayoutToolFingers
|
|
||||||
slideToSettingsScreenEdge:(NSInteger)slideToSettingsScreenEdge
|
|
||||||
slideToSettingsDistance:(CGFloat)slideToSettingsDistance
|
|
||||||
pointerVelocityModeDivider:(CGFloat)pointerVelocityModeDivider
|
|
||||||
touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor
|
|
||||||
mousePointerVelocityFactor:(CGFloat)mousePointerVelocityFactor
|
|
||||||
oscTapExlusionAreaSize:(CGFloat)oscTapExlusionAreaSize
|
|
||||||
reverseMouseWheelDirection:(BOOL)reverseMouseWheelDirection
|
|
||||||
largerStickLR1:(BOOL)largerStickLR1
|
|
||||||
liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard
|
|
||||||
showKeyboardToolbar:(BOOL)showKeyboardToolbar
|
|
||||||
optimizeGames:(BOOL)optimizeGames
|
|
||||||
multiController:(BOOL)multiController
|
|
||||||
swapABXYButtons:(BOOL)swapABXYButtons
|
|
||||||
audioOnPC:(BOOL)audioOnPC
|
|
||||||
preferredCodec:(uint32_t)preferredCodec
|
|
||||||
enableYUV444:(BOOL)enableYUV444
|
|
||||||
useFramePacing:(BOOL)useFramePacing
|
|
||||||
enableHdr:(BOOL)enableHdr
|
|
||||||
btMouseSupport:(BOOL)btMouseSupport
|
|
||||||
// absoluteTouchMode:(BOOL)absoluteTouchMode
|
|
||||||
touchMode:(NSInteger)touchMode
|
|
||||||
statsOverlayLevel:(NSInteger)statsOverlayLevel
|
|
||||||
statsOverlayEnabled:(BOOL)statsOverlayEnabled
|
|
||||||
unlockDisplayOrientation:(BOOL)unlockDisplayOrientation
|
|
||||||
resolutionSelected:(NSInteger)resolutionSelected
|
|
||||||
externalDisplayMode:(NSInteger)externalDisplayMode
|
|
||||||
localMousePointerMode:(NSInteger)localMousePointerMode;
|
|
||||||
|
|
||||||
- (NSArray*) getHosts;
|
|
||||||
- (void) updateHost:(TemporaryHost*)host;
|
|
||||||
- (void) updateAppsForExistingHost:(TemporaryHost *)host;
|
|
||||||
- (void) removeHost:(TemporaryHost*)host;
|
|
||||||
- (void) removeApp:(TemporaryApp*)app;
|
|
||||||
- (Settings*) retrieveSettings;
|
|
||||||
- (void) saveData;
|
|
||||||
- (TemporarySettings*) getSettings;
|
|
||||||
|
|
||||||
- (void) updateUniqueId:(NSString*)uniqueId;
|
|
||||||
- (NSString*) getUniqueId;
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
//
|
|
||||||
// TemporarySettings.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Cameron Gutman on 12/1/15.
|
|
||||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "Settings+CoreDataClass.h"
|
|
||||||
#define RELATIVE_TOUCH 0
|
|
||||||
#define REGULAR_NATIVE_TOUCH 1
|
|
||||||
#define PURE_NATIVE_TOUCH 2
|
|
||||||
#define ABSOLUTE_TOUCH 3
|
|
||||||
#define OSC_TOOL_FINGERS 4
|
|
||||||
|
|
||||||
|
|
||||||
@interface TemporarySettings : NSObject
|
|
||||||
|
|
||||||
@property (nonatomic, retain) Settings * parent;
|
|
||||||
|
|
||||||
@property (nonatomic, retain) NSNumber * bitrate;
|
|
||||||
@property (nonatomic, retain) NSNumber * framerate;
|
|
||||||
@property (nonatomic, retain) NSNumber * height;
|
|
||||||
@property (nonatomic, retain) NSNumber * width;
|
|
||||||
@property (nonatomic, retain) NSNumber * audioConfig;
|
|
||||||
@property (nonatomic, retain) NSNumber * onscreenControls;
|
|
||||||
@property (nonatomic, retain) NSNumber * keyboardToggleFingers;
|
|
||||||
@property (nonatomic, retain) NSNumber * oscLayoutToolFingers;
|
|
||||||
@property (nonatomic, retain) NSNumber * slideToSettingsScreenEdge;
|
|
||||||
@property (nonatomic, retain) NSNumber * slideToSettingsDistance;
|
|
||||||
@property (nonatomic, retain) NSNumber * oscTapExlusionAreaSize;
|
|
||||||
@property (nonatomic, retain) NSNumber * touchPointerVelocityFactor;
|
|
||||||
@property (nonatomic, retain) NSNumber * mousePointerVelocityFactor;
|
|
||||||
@property (nonatomic, retain) NSNumber * pointerVelocityModeDivider;
|
|
||||||
@property (nonatomic, retain) NSString * uniqueId;
|
|
||||||
@property (nonatomic, retain) NSNumber * resolutionSelected;
|
|
||||||
@property (nonatomic, retain) NSNumber * externalDisplayMode;
|
|
||||||
@property (nonatomic, retain) NSNumber * localMousePointerMode;
|
|
||||||
@property (nonatomic) enum {
|
|
||||||
CODEC_PREF_AUTO,
|
|
||||||
CODEC_PREF_H264,
|
|
||||||
CODEC_PREF_HEVC,
|
|
||||||
CODEC_PREF_AV1,
|
|
||||||
} preferredCodec;
|
|
||||||
@property (nonatomic) BOOL enableYUV444;
|
|
||||||
@property (nonatomic) BOOL reverseMouseWheelDirection;
|
|
||||||
@property (nonatomic) BOOL largerStickLR1;
|
|
||||||
@property (nonatomic) BOOL useFramePacing;
|
|
||||||
@property (nonatomic) BOOL multiController;
|
|
||||||
@property (nonatomic) BOOL swapABXYButtons;
|
|
||||||
@property (nonatomic) BOOL playAudioOnPC;
|
|
||||||
@property (nonatomic) BOOL optimizeGames;
|
|
||||||
@property (nonatomic) BOOL enableHdr;
|
|
||||||
@property (nonatomic) BOOL btMouseSupport;
|
|
||||||
// @property (nonatomic) BOOL absoluteTouchMode;
|
|
||||||
@property (nonatomic, retain) NSNumber * touchMode;
|
|
||||||
@property (nonatomic, retain) NSNumber * statsOverlayLevel;
|
|
||||||
@property (nonatomic) BOOL statsOverlayEnabled;
|
|
||||||
@property (nonatomic) BOOL liftStreamViewForKeyboard;
|
|
||||||
@property (nonatomic) BOOL showKeyboardToolbar;
|
|
||||||
@property (nonatomic) BOOL unlockDisplayOrientation;
|
|
||||||
|
|
||||||
- (id) initFromSettings:(Settings*)settings;
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
//
|
|
||||||
// TemporarySettings.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Cameron Gutman on 12/1/15.
|
|
||||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "TemporarySettings.h"
|
|
||||||
#import "OnScreenControls.h"
|
|
||||||
|
|
||||||
@implementation TemporarySettings
|
|
||||||
|
|
||||||
- (id) initFromSettings:(Settings*)settings {
|
|
||||||
self = [self init];
|
|
||||||
|
|
||||||
self.parent = settings;
|
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
|
||||||
// Apply default values from our Root.plist
|
|
||||||
NSString* settingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
|
|
||||||
NSDictionary* settingsData = [NSDictionary dictionaryWithContentsOfFile:[settingsBundle stringByAppendingPathComponent:@"Root.plist"]];
|
|
||||||
NSArray* preferences = [settingsData objectForKey:@"PreferenceSpecifiers"];
|
|
||||||
NSMutableDictionary* defaultsToRegister = [[NSMutableDictionary alloc] initWithCapacity:[preferences count]];
|
|
||||||
for (NSDictionary* prefSpecification in preferences) {
|
|
||||||
NSString* key = [prefSpecification objectForKey:@"Key"];
|
|
||||||
if (key != nil) {
|
|
||||||
[defaultsToRegister setObject:[prefSpecification objectForKey:@"DefaultValue"] forKey:key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[[NSUserDefaults standardUserDefaults] registerDefaults:defaultsToRegister];
|
|
||||||
|
|
||||||
self.bitrate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"bitrate"]];
|
|
||||||
assert([self.bitrate intValue] != 0);
|
|
||||||
self.framerate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framerate"]];
|
|
||||||
assert([self.framerate intValue] != 0);
|
|
||||||
self.audioConfig = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"audioConfig"]];
|
|
||||||
assert([self.audioConfig intValue] != 0);
|
|
||||||
self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"];
|
|
||||||
self.enableYUV444 = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableYUV444"];
|
|
||||||
self.useFramePacing = [[NSUserDefaults standardUserDefaults] integerForKey:@"useFramePacing"] != 0;
|
|
||||||
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
|
|
||||||
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
|
|
||||||
self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"];
|
|
||||||
self.multiController = [[NSUserDefaults standardUserDefaults] boolForKey:@"multipleControllers"];
|
|
||||||
self.swapABXYButtons = [[NSUserDefaults standardUserDefaults] boolForKey:@"swapABXYButtons"];
|
|
||||||
self.btMouseSupport = [[NSUserDefaults standardUserDefaults] boolForKey:@"btMouseSupport"];
|
|
||||||
self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
|
|
||||||
|
|
||||||
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
|
|
||||||
switch (_screenSize) {
|
|
||||||
case 0:
|
|
||||||
self.height = [NSNumber numberWithInteger:720];
|
|
||||||
self.width = [NSNumber numberWithInteger:1280];
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
self.height = [NSNumber numberWithInteger:1080];
|
|
||||||
self.width = [NSNumber numberWithInteger:1920];
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
self.height = [NSNumber numberWithInteger:2160];
|
|
||||||
self.width = [NSNumber numberWithInteger:3840];
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
self.height = [NSNumber numberWithInteger:1440];
|
|
||||||
self.width = [NSNumber numberWithInteger:2560];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
self.onscreenControls = [NSNumber numberWithInteger:OnScreenControlsLevelOff];
|
|
||||||
#else
|
|
||||||
self.bitrate = settings.bitrate;
|
|
||||||
self.framerate = settings.framerate;
|
|
||||||
self.height = settings.height;
|
|
||||||
self.width = settings.width;
|
|
||||||
self.audioConfig = settings.audioConfig;
|
|
||||||
self.preferredCodec = settings.preferredCodec;
|
|
||||||
self.enableYUV444 = settings.enableYUV444;
|
|
||||||
self.useFramePacing = settings.useFramePacing;
|
|
||||||
self.playAudioOnPC = settings.playAudioOnPC;
|
|
||||||
self.enableHdr = settings.enableHdr;
|
|
||||||
self.optimizeGames = settings.optimizeGames;
|
|
||||||
self.multiController = settings.multiController;
|
|
||||||
self.swapABXYButtons = settings.swapABXYButtons;
|
|
||||||
self.onscreenControls = settings.onscreenControls;
|
|
||||||
self.reverseMouseWheelDirection = settings.reverseMouseWheelDirection;
|
|
||||||
self.largerStickLR1 = settings.largerStickLR1;
|
|
||||||
self.btMouseSupport = settings.btMouseSupport;
|
|
||||||
// self.absoluteTouchMode = settings.absoluteTouchMode;
|
|
||||||
self.touchMode = settings.touchMode;
|
|
||||||
self.statsOverlayLevel = settings.statsOverlayLevel;
|
|
||||||
self.statsOverlayEnabled = settings.statsOverlayEnabled;
|
|
||||||
self.keyboardToggleFingers = settings.keyboardToggleFingers;
|
|
||||||
self.oscLayoutToolFingers = settings.oscLayoutToolFingers;
|
|
||||||
self.slideToSettingsScreenEdge = settings.slideToSettingsScreenEdge;
|
|
||||||
self.slideToSettingsDistance = settings.slideToSettingsDistance;
|
|
||||||
self.liftStreamViewForKeyboard = settings.liftStreamViewForKeyboard;
|
|
||||||
self.showKeyboardToolbar = settings.showKeyboardToolbar;
|
|
||||||
self.oscTapExlusionAreaSize = settings.oscTapExlusionAreaSize;
|
|
||||||
self.touchPointerVelocityFactor = settings.touchPointerVelocityFactor;
|
|
||||||
self.mousePointerVelocityFactor = settings.mousePointerVelocityFactor;
|
|
||||||
self.pointerVelocityModeDivider = settings.pointerVelocityModeDivider;
|
|
||||||
self.unlockDisplayOrientation = settings.unlockDisplayOrientation;
|
|
||||||
self.resolutionSelected = settings.resolutionSelected;
|
|
||||||
self.externalDisplayMode = settings.externalDisplayMode;
|
|
||||||
self.localMousePointerMode = settings.localMousePointerMode;
|
|
||||||
#endif
|
|
||||||
self.uniqueId = settings.uniqueId;
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,149 +0,0 @@
|
|||||||
//
|
|
||||||
// AbsoluteTouchHandler.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Cameron Gutman on 11/1/20.
|
|
||||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "AbsoluteTouchHandler.h"
|
|
||||||
|
|
||||||
#include <Limelight.h>
|
|
||||||
|
|
||||||
// How long the fingers must be stationary to start a right click
|
|
||||||
#define LONG_PRESS_ACTIVATION_DELAY 0.650f
|
|
||||||
|
|
||||||
// How far the finger can move before it cancels a right click
|
|
||||||
#define LONG_PRESS_ACTIVATION_DELTA 0.01f
|
|
||||||
|
|
||||||
// How long the double tap deadzone stays in effect between touch up and touch down
|
|
||||||
#define DOUBLE_TAP_DEAD_ZONE_DELAY 0.250f
|
|
||||||
|
|
||||||
// How far the finger can move before it can override the double tap deadzone
|
|
||||||
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
|
|
||||||
|
|
||||||
@implementation AbsoluteTouchHandler {
|
|
||||||
StreamView* streamView;
|
|
||||||
|
|
||||||
NSTimer* longPressTimer;
|
|
||||||
UITouch* lastTouchDown;
|
|
||||||
CGPoint lastTouchDownLocation;
|
|
||||||
UITouch* lastTouchUp;
|
|
||||||
CGPoint lastTouchUpLocation;
|
|
||||||
|
|
||||||
// upper screen edge check
|
|
||||||
bool touchPointSpawnedAtUpperScreenEdge;
|
|
||||||
CGFloat slideGestureVerticalThreshold;
|
|
||||||
CGFloat screenWidthWithThreshold;
|
|
||||||
CGFloat EDGE_TOLERANCE;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id)initWithView:(StreamView*)view {
|
|
||||||
self = [self init];
|
|
||||||
self->streamView = view;
|
|
||||||
|
|
||||||
// upper screen check
|
|
||||||
EDGE_TOLERANCE = 15.0;
|
|
||||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
|
||||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
|
||||||
self->touchPointSpawnedAtUpperScreenEdge = false;
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)onLongPressStart:(NSTimer*)timer {
|
|
||||||
// Raise the left click and start a right click
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
|
|
||||||
CGPoint initialPoint = [[touches anyObject] locationInView:streamView];
|
|
||||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
|
||||||
self->touchPointSpawnedAtUpperScreenEdge = true;
|
|
||||||
return; // we're done here. this touch event will not be sent to the remote PC.
|
|
||||||
}
|
|
||||||
|
|
||||||
touchPointSpawnedAtUpperScreenEdge = false; // reset this flag immediately if we get a touch event passing the check above, this fixes irresponsive touch after closing the command tool menu.
|
|
||||||
|
|
||||||
// Ignore touch down events with more than one finger
|
|
||||||
if ([[event allTouches] count] > 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UITouch* touch = [touches anyObject];
|
|
||||||
CGPoint touchLocation = [touch locationInView:streamView];
|
|
||||||
|
|
||||||
// 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 ||
|
|
||||||
sqrt(pow((touchLocation.x / streamView.bounds.size.width) - (lastTouchUpLocation.x / streamView.bounds.size.width), 2) +
|
|
||||||
pow((touchLocation.y / streamView.bounds.size.height) - (lastTouchUpLocation.y / streamView.bounds.size.height), 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
|
|
||||||
[streamView updateCursorLocation:touchLocation isMouse:NO];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Press the left button down
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
|
||||||
|
|
||||||
// Start the long press timer
|
|
||||||
longPressTimer = [NSTimer scheduledTimerWithTimeInterval:LONG_PRESS_ACTIVATION_DELAY
|
|
||||||
target:self
|
|
||||||
selector:@selector(onLongPressStart:)
|
|
||||||
userInfo:nil
|
|
||||||
repeats:NO];
|
|
||||||
|
|
||||||
lastTouchDown = touch;
|
|
||||||
lastTouchDownLocation = touchLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
|
|
||||||
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
|
||||||
|
|
||||||
// Ignore touch move events with more than one finger
|
|
||||||
if ([[event allTouches] count] > 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UITouch* touch = [touches anyObject];
|
|
||||||
CGPoint touchLocation = [touch locationInView:streamView];
|
|
||||||
|
|
||||||
if (sqrt(pow((touchLocation.x / streamView.bounds.size.width) - (lastTouchDownLocation.x / streamView.bounds.size.width), 2) +
|
|
||||||
pow((touchLocation.y / streamView.bounds.size.height) - (lastTouchDownLocation.y / streamView.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) {
|
|
||||||
// Moved too far since touch down. Cancel the long press timer.
|
|
||||||
[longPressTimer invalidate];
|
|
||||||
longPressTimer = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
[streamView updateCursorLocation:[[touches anyObject] locationInView:streamView] isMouse:NO];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
|
|
||||||
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
|
||||||
|
|
||||||
// Only fire this logic if all touches have ended
|
|
||||||
if ([[event allTouches] count] == [touches count]) {
|
|
||||||
// Cancel the long press timer
|
|
||||||
[longPressTimer invalidate];
|
|
||||||
longPressTimer = nil;
|
|
||||||
|
|
||||||
// Left button up on finger up
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
|
||||||
|
|
||||||
// Raise right button too in case we triggered a long press gesture
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
|
||||||
|
|
||||||
// Remember this last touch for touch-down deadzoning
|
|
||||||
lastTouchUp = [touches anyObject];
|
|
||||||
lastTouchUpLocation = [lastTouchUp locationInView:streamView];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
// Treat this as a normal touchesEnded event
|
|
||||||
[self touchesEnded:touches withEvent:event];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,492 +0,0 @@
|
|||||||
//
|
|
||||||
// KeyManager.swift
|
|
||||||
// Moonlight-ZWM
|
|
||||||
//
|
|
||||||
// Created by ZWM on 2024/7/23.
|
|
||||||
// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
// Define the RemoteCommand class
|
|
||||||
@objc public class RemoteCommand: NSObject, NSSecureCoding {
|
|
||||||
// MARK: - NSSecureCoding
|
|
||||||
|
|
||||||
public static var supportsSecureCoding: Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Properties
|
|
||||||
|
|
||||||
@objc var keyboardCmdString: String
|
|
||||||
@objc var alias: String
|
|
||||||
|
|
||||||
// MARK: - Initialization
|
|
||||||
|
|
||||||
init(keyboardCmdString: String, alias: String) {
|
|
||||||
self.keyboardCmdString = keyboardCmdString
|
|
||||||
self.alias = alias
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - NSSecureCoding
|
|
||||||
|
|
||||||
required public init?(coder: NSCoder) {
|
|
||||||
guard let keyboardCmdString = coder.decodeObject(of: NSString.self, forKey: "keyboardCmdString") as String?,
|
|
||||||
let alias = coder.decodeObject(of: NSString.self, forKey: "alias") as String? else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.keyboardCmdString = keyboardCmdString
|
|
||||||
self.alias = alias
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(with coder: NSCoder) {
|
|
||||||
coder.encode(keyboardCmdString, forKey: "keyboardCmdString")
|
|
||||||
coder.encode(alias, forKey: "alias")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Define the CommandManager class
|
|
||||||
@objc public class CommandManager: NSObject {
|
|
||||||
@objc public static let shared = CommandManager()
|
|
||||||
|
|
||||||
@objc public static let mouseButtonMappings: [String: Int32] = ["M_LEFT" : BUTTON_LEFT,
|
|
||||||
"M_MIDDLE" : BUTTON_MIDDLE,
|
|
||||||
"M_RIGHT" : BUTTON_RIGHT,
|
|
||||||
"M_X1" : BUTTON_X1,
|
|
||||||
"M_X2" : BUTTON_X2]
|
|
||||||
|
|
||||||
@objc public static let oscButtonMappings: [String: Int32] = ["OSCA" : A_FLAG,
|
|
||||||
"OSCB" : B_FLAG,
|
|
||||||
"OSCX" : X_FLAG,
|
|
||||||
"OSCY" : Y_FLAG,
|
|
||||||
"OSCL1" : LB_FLAG,
|
|
||||||
"OSCR1" : RB_FLAG,
|
|
||||||
"OSCL3" : LS_CLK_FLAG,
|
|
||||||
"OSCR3" : RS_CLK_FLAG,
|
|
||||||
"OSCSTART" : PLAY_FLAG,
|
|
||||||
"OSCSELECT" : BACK_FLAG,
|
|
||||||
"OSCUP" : UP_FLAG,
|
|
||||||
"OSCDOWN" : DOWN_FLAG,
|
|
||||||
"OSCLEFT" : LEFT_FLAG,
|
|
||||||
"OSCRIGHT" : RIGHT_FLAG,
|
|
||||||
"OSCL2" : 0,
|
|
||||||
"OSCR2" : 0]
|
|
||||||
|
|
||||||
@objc public static let oscRectangleButtonCmds: [String] = [
|
|
||||||
"OSCUP",
|
|
||||||
"OSCDOWN",
|
|
||||||
"OSCLEFT",
|
|
||||||
"OSCRIGHT",
|
|
||||||
"OSCSTART",
|
|
||||||
"OSCSELECT"]
|
|
||||||
|
|
||||||
@objc public static let touchPadCmds: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD", "MOUSEPAD", "DPAD", "WASDPAD", "ARROWPAD"]
|
|
||||||
@objc public static let stickTouchPads: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD"]
|
|
||||||
@objc public static let nonVectorStickPads: [String] = ["LSPAD", "RSPAD"]
|
|
||||||
@objc public static let specialOverlayButtonCmds: [String] = ["SETTINGS"]
|
|
||||||
|
|
||||||
static let keyboardButtonMappings: [String: Int16] = [
|
|
||||||
// Windows Key Codes
|
|
||||||
"CTRL": 0x11, // VK_CONTROL
|
|
||||||
"SHIFT": 0x10, // VK_SHIFT
|
|
||||||
"ALT": 0x12, // VK_MENU
|
|
||||||
"F1": 0x70, // VK_F1
|
|
||||||
"F2": 0x71, // VK_F2
|
|
||||||
"F3": 0x72, // VK_F3
|
|
||||||
"F4": 0x73, // VK_F4
|
|
||||||
"F5": 0x74, // VK_F5
|
|
||||||
"F6": 0x75, // VK_F6
|
|
||||||
"F7": 0x76, // VK_F7
|
|
||||||
"F8": 0x77, // VK_F8
|
|
||||||
"F9": 0x78, // VK_F9
|
|
||||||
"F10": 0x79, // VK_F10
|
|
||||||
"F11": 0x7A, // VK_F11
|
|
||||||
"F12": 0x7B, // VK_F12
|
|
||||||
"A": 0x41, // 'A' key
|
|
||||||
"B": 0x42, // 'B' key
|
|
||||||
"C": 0x43, // 'C' key
|
|
||||||
"D": 0x44, // 'D' key
|
|
||||||
"E": 0x45, // 'E' key
|
|
||||||
"F": 0x46, // 'F' key
|
|
||||||
"G": 0x47, // 'G' key
|
|
||||||
"H": 0x48, // 'H' key
|
|
||||||
"I": 0x49, // 'I' key
|
|
||||||
"J": 0x4A, // 'J' key
|
|
||||||
"K": 0x4B, // 'K' key
|
|
||||||
"L": 0x4C, // 'L' key
|
|
||||||
"M": 0x4D, // 'M' key
|
|
||||||
"N": 0x4E, // 'N' key
|
|
||||||
"O": 0x4F, // 'O' key
|
|
||||||
"P": 0x50, // 'P' key
|
|
||||||
"Q": 0x51, // 'Q' key
|
|
||||||
"R": 0x52, // 'R' key
|
|
||||||
"S": 0x53, // 'S' key
|
|
||||||
"T": 0x54, // 'T' key
|
|
||||||
"U": 0x55, // 'U' key
|
|
||||||
"V": 0x56, // 'V' key
|
|
||||||
"W": 0x57, // 'W' key
|
|
||||||
"X": 0x58, // 'X' key
|
|
||||||
"Y": 0x59, // 'Y' key
|
|
||||||
"Z": 0x5A, // 'Z' key
|
|
||||||
"0": 0x30, // '0' key
|
|
||||||
"1": 0x31, // '1' key
|
|
||||||
"2": 0x32, // '2' key
|
|
||||||
"3": 0x33, // '3' key
|
|
||||||
"4": 0x34, // '4' key
|
|
||||||
"5": 0x35, // '5' key
|
|
||||||
"6": 0x36, // '6' key
|
|
||||||
"7": 0x37, // '7' key
|
|
||||||
"8": 0x38, // '8' key
|
|
||||||
"9": 0x39, // '9' key
|
|
||||||
"ESC": 0x1B, // VK_ESCAPE
|
|
||||||
"SPACE": 0x20, // VK_SPACE
|
|
||||||
"ENTER": 0x0D, // VK_RETURN
|
|
||||||
"TAB": 0x09, // VK_TAB
|
|
||||||
"BACKSPACE": 0x08, // VK_BACK
|
|
||||||
"INSERT": 0x2D, // VK_INSERT
|
|
||||||
"DEL": 0x2E, // VK_DELETE
|
|
||||||
"HOME": 0x24, // VK_HOME
|
|
||||||
"END": 0x23, // VK_END
|
|
||||||
"PG_UP": 0x21, // VK_PRIOR
|
|
||||||
"PG_DOWN": 0x22, // VK_NEXT
|
|
||||||
"UP_ARROW": 0x26, // VK_UP
|
|
||||||
"DOWN_ARROW": 0x28, // VK_DOWN
|
|
||||||
"LEFT_ARROW": 0x25, // VK_LEFT
|
|
||||||
"RIGHT_ARROW": 0x27, // VK_RIGHT
|
|
||||||
"NUM_LCK": 0x90, // VK_NUMLOCK
|
|
||||||
"SCR_LCK": 0x91, // VK_SCROLL
|
|
||||||
"CAPS_LOCK": 0x14, // VK_CAPITAL
|
|
||||||
"PAUSE": 0x13, // VK_PAUSE
|
|
||||||
"PR_SCR": 0x2C, // VK_SNAPSHOT
|
|
||||||
"NUMPAD0": 0x60, // VK_NUMPAD0
|
|
||||||
"NUMPAD1": 0x61, // VK_NUMPAD1
|
|
||||||
"NUMPAD2": 0x62, // VK_NUMPAD2
|
|
||||||
"NUMPAD3": 0x63, // VK_NUMPAD3
|
|
||||||
"NUMPAD4": 0x64, // VK_NUMPAD4
|
|
||||||
"NUMPAD5": 0x65, // VK_NUMPAD5
|
|
||||||
"NUMPAD6": 0x66, // VK_NUMPAD6
|
|
||||||
"NUMPAD7": 0x67, // VK_NUMPAD7
|
|
||||||
"NUMPAD8": 0x68, // VK_NUMPAD8
|
|
||||||
"NUMPAD9": 0x69, // VK_NUMPAD9
|
|
||||||
"MULTIPLY": 0x6A, // VK_MULTIPLY
|
|
||||||
"ADD": 0x6B, // VK_ADD
|
|
||||||
"SUBTRACT": 0x6D, // VK_SUBTRACT
|
|
||||||
"DECIMAL": 0x6E, // VK_DECIMAL
|
|
||||||
"DIVIDE": 0x6F, // VK_DIVIDE
|
|
||||||
"SEMI_COLON": 0xBA, // VK_OEM_1
|
|
||||||
"EQUALS": 0xBB, // VK_OEM_PLUS
|
|
||||||
"COMMA": 0xBC, // VK_OEM_COMMA
|
|
||||||
"MINUS": 0xBD, // VK_OEM_MINUS
|
|
||||||
"PERIOD": 0xBE, // VK_OEM_PERIOD
|
|
||||||
"FORWARD_SLASH": 0xBF, // VK_OEM_2
|
|
||||||
"GRAVE_ACCENT": 0xC0, // VK_OEM_3
|
|
||||||
"OPEN_BRACKET": 0xDB, // VK_OEM_4
|
|
||||||
"BACKSLASH": 0xDC, // VK_OEM_5
|
|
||||||
"CLOSE_BRACKET": 0xDD, // VK_OEM_6
|
|
||||||
"SINGLE_QUOTE": 0xDE, // VK_OEM_7
|
|
||||||
"VOLUME_MUTE": 0xAD, // VK_VOLUME_MUTE
|
|
||||||
"VOLUME_DOWN": 0xAE, // VK_VOLUME_DOWN
|
|
||||||
"VOLUME_UP": 0xAF, // VK_VOLUME_UP
|
|
||||||
"MEDIA_NEXT": 0xB0, // VK_MEDIA_NEXT_TRACK
|
|
||||||
"MEDIA_PREV": 0xB1, // VK_MEDIA_PREV_TRACK
|
|
||||||
"MEDIA_STOP": 0xB2, // VK_MEDIA_STOP
|
|
||||||
"MEDIA_PLAY_PAUSE": 0xB3, // VK_MEDIA_PLAY_PAUSE
|
|
||||||
"LAUNCH_MAIL": 0xB4, // VK_LAUNCH_MAIL
|
|
||||||
"LAUNCH_MEDIA_SELECT": 0xB5, // VK_LAUNCH_MEDIA_SELECT
|
|
||||||
"LAUNCH_APP1": 0xB6, // VK_LAUNCH_APP1
|
|
||||||
"LAUNCH_APP2": 0xB7, // VK_LAUNCH_APP2
|
|
||||||
"WIN": 0x5B,
|
|
||||||
"LEFT_WIN": 0x5B, // VK_LWIN
|
|
||||||
"RIGHT_WIN": 0x5C, // VK_RWIN
|
|
||||||
"APPS": 0x5D, // VK_APPS
|
|
||||||
|
|
||||||
// macOS Key Codes
|
|
||||||
"CMD": 0x37, // ⌘ Command
|
|
||||||
"OPT": 0x3A, // ⌥ Option
|
|
||||||
"CONTROL": 0x3B, // ⌃ Control
|
|
||||||
"SHIFT_MAC": 0x38, // ⇧ Shift
|
|
||||||
"FUNCTION": 0x3F, // fn
|
|
||||||
"DELETE_MAC": 0x75, // Forward Delete
|
|
||||||
"RETURN_MAC": 0x24, // Return
|
|
||||||
"ENTER_MAC": 0x4C, // Enter
|
|
||||||
"ESCAPE_MAC": 0x35, // Escape
|
|
||||||
"TAB_MAC": 0x30, // Tab
|
|
||||||
"SPACE_MAC": 0x31, // Space
|
|
||||||
"UP_ARROW_MAC": 0x7E, // Up Arrow
|
|
||||||
"DOWN_ARROW_MAC": 0x7D, // Down Arrow
|
|
||||||
"LEFT_ARROW_MAC": 0x7B, // Left Arrow
|
|
||||||
"RIGHT_ARROW_MAC": 0x7C, // Right Arrow
|
|
||||||
"F1_MAC": 0x7A, // F1
|
|
||||||
"F2_MAC": 0x78, // F2
|
|
||||||
"F3_MAC": 0x63, // F3
|
|
||||||
"F4_MAC": 0x76, // F4
|
|
||||||
"F5_MAC": 0x60, // F5
|
|
||||||
"F6_MAC": 0x61, // F6
|
|
||||||
"F7_MAC": 0x62, // F7
|
|
||||||
"F8_MAC": 0x64, // F8
|
|
||||||
"F9_MAC": 0x65, // F9
|
|
||||||
"F10_MAC": 0x6D, // F10
|
|
||||||
"F11_MAC": 0x67, // F11
|
|
||||||
"F12_MAC": 0x6F, // F12
|
|
||||||
"0_MAC": 0x52, // 0
|
|
||||||
"1_MAC": 0x53, // 1
|
|
||||||
"2_MAC": 0x54, // 2
|
|
||||||
"3_MAC": 0x55, // 3
|
|
||||||
"4_MAC": 0x56, // 4
|
|
||||||
"5_MAC": 0x57, // 5
|
|
||||||
"6_MAC": 0x58, // 6
|
|
||||||
"7_MAC": 0x59, // 7
|
|
||||||
"8_MAC": 0x5A, // 8
|
|
||||||
"9_MAC": 0x5B, // 9
|
|
||||||
"NUMPAD0_MAC": 0x4F, // Numpad 0
|
|
||||||
"NUMPAD1_MAC": 0x50, // Numpad 1
|
|
||||||
"NUMPAD2_MAC": 0x51, // Numpad 2
|
|
||||||
"NUMPAD3_MAC": 0x52, // Numpad 3
|
|
||||||
"NUMPAD4_MAC": 0x53, // Numpad 4
|
|
||||||
"NUMPAD5_MAC": 0x54, // Numpad 5
|
|
||||||
"NUMPAD6_MAC": 0x55, // Numpad 6
|
|
||||||
"NUMPAD7_MAC": 0x56, // Numpad 7
|
|
||||||
"NUMPAD8_MAC": 0x57, // Numpad 8
|
|
||||||
"NUMPAD9_MAC": 0x58, // Numpad 9
|
|
||||||
"NUMPAD_ADD_MAC": 0x45, // Numpad Add
|
|
||||||
"NUMPAD_SUBTRACT_MAC": 0x4A, // Numpad Subtract
|
|
||||||
"NUMPAD_MULTIPLY_MAC": 0x43, // Numpad Multiply
|
|
||||||
"NUMPAD_DIVIDE_MAC": 0x4B, // Numpad Divide
|
|
||||||
"NUMPAD_DECIMAL_MAC": 0x41, // Numpad Decimal
|
|
||||||
]
|
|
||||||
|
|
||||||
private var commands: [RemoteCommand] = []
|
|
||||||
|
|
||||||
public weak var viewController: CommandManagerViewController?
|
|
||||||
|
|
||||||
private override init() {
|
|
||||||
super.init()
|
|
||||||
loadCommands()
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc static func presetDefaultCommands() {
|
|
||||||
let defaults = UserDefaults.standard
|
|
||||||
//if true { // save default entries if the data is empty.
|
|
||||||
if defaults.data(forKey: "savedCommands") == nil { // save default entries if the data is empty.
|
|
||||||
let defaultCommands: [RemoteCommand] = [
|
|
||||||
RemoteCommand(keyboardCmdString: "WIN", alias: "WIN"),
|
|
||||||
RemoteCommand(keyboardCmdString: "F11", alias: "F11"),
|
|
||||||
RemoteCommand(keyboardCmdString: "ESC", alias: "ESC"),
|
|
||||||
RemoteCommand(keyboardCmdString: "CTRL+SHIFT+ESC", alias: "任务管理器(Task Manager)"),
|
|
||||||
RemoteCommand(keyboardCmdString: "ALT+F1", alias: "N卡截图(Nvidia Screenshot)"),
|
|
||||||
RemoteCommand(keyboardCmdString: "ALT+F9", alias: "N卡录屏(Nvidia Screen Recording)"),
|
|
||||||
RemoteCommand(keyboardCmdString: "ALT+F4", alias: "关闭窗口(ALT+F4)"),
|
|
||||||
RemoteCommand(keyboardCmdString: "CTRL+A", alias: "全选(Select All)"),
|
|
||||||
RemoteCommand(keyboardCmdString: "CTRL+C", alias: "复制(Copy)"),
|
|
||||||
RemoteCommand(keyboardCmdString: "CTRL+V", alias: "粘贴(Paste)"),
|
|
||||||
RemoteCommand(keyboardCmdString: "WIN+D", alias: "切换桌面(Switch to Desktop)"),
|
|
||||||
RemoteCommand(keyboardCmdString: "WIN+P", alias: "多显模式(Project)"),
|
|
||||||
RemoteCommand(keyboardCmdString: "WIN+G", alias: "Xbox Game Bar"),
|
|
||||||
RemoteCommand(keyboardCmdString: "SHIFT+TAB", alias: "Steam Overlay"),
|
|
||||||
]
|
|
||||||
|
|
||||||
let data = try? NSKeyedArchiver.archivedData(withRootObject: defaultCommands, requiringSecureCoding: false)
|
|
||||||
defaults.set(data, forKey: "savedCommands")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func createTestKeyMappings() -> [String: Int16] {
|
|
||||||
return CommandManager.keyboardButtonMappings
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractKeyStrings from keyboardCMDString
|
|
||||||
@objc public func extractKeyStringsFromComboCommand(from input: String) -> [String]? {
|
|
||||||
let keys = CommandManager.keyboardButtonMappings.keys.joined(separator: "|")
|
|
||||||
let pattern = "^(?:(\(keys))(?:\\+(\(keys))*)*)$"
|
|
||||||
|
|
||||||
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
|
||||||
print("Failed to create regex")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let range = NSRange(location: 0, length: input.utf16.count)
|
|
||||||
guard let match = regex.firstMatch(in: input, options: [], range: range) else {
|
|
||||||
print("No match found for input: \(input)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
print("Regex matched for input: \(input)")
|
|
||||||
|
|
||||||
let matchedString = (input as NSString).substring(with: match.range(at: 0))
|
|
||||||
let keyStrings = matchedString.split(separator: "+").map { String($0) }
|
|
||||||
|
|
||||||
guard !keyStrings.isEmpty else {
|
|
||||||
print("No key strings found in the matched string")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var validKeyStrings: [String] = []
|
|
||||||
|
|
||||||
for key in keyStrings {
|
|
||||||
if CommandManager.keyboardButtonMappings.keys.contains(key) {
|
|
||||||
validKeyStrings.append(key)
|
|
||||||
} else {
|
|
||||||
print(" '\(key)' is not defined in key mappings")
|
|
||||||
return nil //treat any illegal string as a whole
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if validKeyStrings.isEmpty {
|
|
||||||
print("No valid key strings found in the matched string")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for (index, key) in validKeyStrings.enumerated() {
|
|
||||||
print("Valid Key \(index): \(key)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return validKeyStrings
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//super combo key button strings
|
|
||||||
@objc public func extractKeyStringsFromComboKeys(from input: String) -> [String]? {
|
|
||||||
let combinedStrings = [CommandManager.keyboardButtonMappings.keys.map { $0 as String },
|
|
||||||
CommandManager.oscButtonMappings.keys.map { $0 as String },
|
|
||||||
CommandManager.mouseButtonMappings.keys.map { $0 as String }]
|
|
||||||
.lazy // 零内存开销
|
|
||||||
.flatMap { $0 } // 三维展开
|
|
||||||
.map(String.init(describing:)) // 安全类型转换
|
|
||||||
|
|
||||||
let keys = combinedStrings.joined(separator: "|")
|
|
||||||
// print(keys)
|
|
||||||
let pattern = "^(?:(\(keys))(?:\\-(\(keys))*)*)$"
|
|
||||||
|
|
||||||
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
|
||||||
print("Failed to create regex")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let range = NSRange(location: 0, length: input.utf16.count)
|
|
||||||
guard let match = regex.firstMatch(in: input, options: [], range: range) else {
|
|
||||||
print("No match found for input: \(input)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
print("Regex matched for input: \(input)")
|
|
||||||
|
|
||||||
let matchedString = (input as NSString).substring(with: match.range(at: 0))
|
|
||||||
let keyStrings = matchedString.split(separator: "-").map { String($0) }
|
|
||||||
|
|
||||||
guard !keyStrings.isEmpty else {
|
|
||||||
print("No key strings found in the matched string")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var validKeyStrings: [String] = []
|
|
||||||
|
|
||||||
for key in keyStrings {
|
|
||||||
validKeyStrings.append(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
if validKeyStrings.isEmpty {
|
|
||||||
print("No valid key strings found in the matched string")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for (index, key) in validKeyStrings.enumerated() {
|
|
||||||
print("Valid Key \(index): \(key)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return validKeyStrings
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func addCommand(_ command: RemoteCommand) -> Bool {
|
|
||||||
command.keyboardCmdString = command.keyboardCmdString.uppercased() // convert all letters to upper case
|
|
||||||
if(command.alias.trimmingCharacters(in: .whitespacesAndNewlines).count == 0) {command.alias = command.keyboardCmdString} // copy cmd string as alias when alias is empty
|
|
||||||
let keyStrings = extractKeyStringsFromComboCommand(from: command.keyboardCmdString)
|
|
||||||
if (keyStrings == nil) {return false} // in case of non-keyboard command strings, return false
|
|
||||||
commands.append(command)
|
|
||||||
saveCommands()
|
|
||||||
viewController?.reloadTableView() // don't know why but this reload has to be called from the CommandManager, doesn't work by calling it in the viewcontroller, probably related with the dialog box.
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func deleteCommand(at index: Int) {
|
|
||||||
guard index >= 0 && index < commands.count else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
commands.remove(at: index)
|
|
||||||
saveCommands()
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func getAllCommands() -> [RemoteCommand] {
|
|
||||||
return commands
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadCommands() {
|
|
||||||
if let savedCommandsData = UserDefaults.standard.data(forKey: "savedCommands") {
|
|
||||||
do {
|
|
||||||
// Attempt to unarchive the data into an array of RemoteCommand
|
|
||||||
if let savedCommands = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, RemoteCommand.self], from: savedCommandsData) as? [RemoteCommand] {
|
|
||||||
// Assign the unarchived commands to your property
|
|
||||||
print(" Assign the unarchived commands to your property ")
|
|
||||||
commands = savedCommands
|
|
||||||
} else {
|
|
||||||
// Handle the case where the data could not be unarchived into the expected type
|
|
||||||
print("Data could not be unarchived into [RemoteCommand]")
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Handle any errors that occur during unarchiving
|
|
||||||
print("Failed to unarchive savedCommands with error: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private func saveCommands() {
|
|
||||||
if let data = try? NSKeyedArchiver.archivedData(withRootObject: commands, requiringSecureCoding: false) {
|
|
||||||
UserDefaults.standard.set(data, forKey: "savedCommands")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func sendKeyComboCommand(keyboardCmdStrings: [String], delay: TimeInterval = 0.2, index: Int = 0) { // we need a large delay for WAN streaming
|
|
||||||
// 如果已处理完所有按键,则开始释放按键
|
|
||||||
guard index < keyboardCmdStrings.count else {
|
|
||||||
// 释放按键
|
|
||||||
for keyStr in keyboardCmdStrings.reversed() { // 从后往前释放按键
|
|
||||||
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
|
|
||||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_UP), 0) // 释放按键
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前按键的映射值
|
|
||||||
if let keyCode = CommandManager.keyboardButtonMappings[keyboardCmdStrings[index]] {
|
|
||||||
// 发送当前按键的按下事件
|
|
||||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_DOWN), 0)
|
|
||||||
|
|
||||||
// 延迟后递归处理下一个按键
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
|
||||||
self.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index + 1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print("No mapping found for \(keyboardCmdStrings[index])")
|
|
||||||
// 如果当前按键没有映射,跳过当前按键并继续下一个
|
|
||||||
self.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func sendKeyComboDown(keyboardCmdStrings: [String]) { // we need a large delay for WAN streaming
|
|
||||||
for keyStr in keyboardCmdStrings {
|
|
||||||
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
|
|
||||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_DOWN), 0) // 释放按键
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func sendKeyComboUp(keyboardCmdStrings: [String]) { // we need a large delay for WAN streaming
|
|
||||||
for keyStr in keyboardCmdStrings {
|
|
||||||
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
|
|
||||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_UP), 0) // 释放按键
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
//
|
|
||||||
// ControllerSupport.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Cameron Gutman on 10/20/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "StreamConfiguration.h"
|
|
||||||
#import "Controller.h"
|
|
||||||
|
|
||||||
@class OnScreenControls;
|
|
||||||
|
|
||||||
@protocol ControllerSupportDelegate <NSObject>
|
|
||||||
|
|
||||||
- (void) gamepadPresenceChanged;
|
|
||||||
- (void) mousePresenceChanged;
|
|
||||||
- (void) streamExitRequested;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface ControllerSupport : NSObject
|
|
||||||
|
|
||||||
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
|
|
||||||
-(void) connectionEstablished;
|
|
||||||
|
|
||||||
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc;
|
|
||||||
-(void) cleanup;
|
|
||||||
-(Controller*) getOscController;
|
|
||||||
|
|
||||||
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y;
|
|
||||||
-(void) updateRightStick:(Controller*)controller x:(short)x y:(short)y;
|
|
||||||
|
|
||||||
-(void) updateLeftTrigger:(Controller*)controller left:(unsigned char)left;
|
|
||||||
-(void) updateRightTrigger:(Controller*)controller right:(unsigned char)right;
|
|
||||||
-(void) updateTriggers:(Controller*)controller left:(unsigned char)left right:(unsigned char)right;
|
|
||||||
|
|
||||||
-(void) updateButtonFlags:(Controller*)controller flags:(int)flags;
|
|
||||||
-(void) setButtonFlag:(Controller*)controller flags:(int)flags;
|
|
||||||
-(void) clearButtonFlag:(Controller*)controller flags:(int)flags;
|
|
||||||
|
|
||||||
-(void) updateFinished:(Controller*)controller;
|
|
||||||
|
|
||||||
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
|
|
||||||
-(void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger;
|
|
||||||
-(void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz;
|
|
||||||
-(void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b;
|
|
||||||
|
|
||||||
-(uint16_t) getActiveGamepadMask;
|
|
||||||
|
|
||||||
+(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig;
|
|
||||||
|
|
||||||
-(NSUInteger) getConnectedGamepadCount;
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
//
|
|
||||||
// OSCProfile.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Long Le on 12/22/22.
|
|
||||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "OnScreenButtonState.h"
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
/**
|
|
||||||
This object contains information pertaining to any of the user created, custom on screen controller layout configurations, or 'profiles.' The object contains a 'name' property for easy reference, as well as an 'isSelected' property which is used to determine whether this particular custom OSC layout should show on screen during game stream view. Only one 'OSCProfile' is set to 'isSelected' at any given time. The object also contains an array of 'OnScreenButtonStates' which provides information that allows us to move and hide/unhide each of the 19 on screen buttons. Note that the 'buttonStates' property should contain an NSMutableArray of ENCODED 'OnScreenButtonState' objects. This allows us to save the 'OSCProfile' object to NSUserDefaults.
|
|
||||||
Additionally the 'OSCProfile' object adopts encoding and decoding protocols so that we can encode the object before saving it to NSUserDefaults. By saving this object to NSUserDefaults we allow the user to save and load their custom on screen controller button layouts between app launches
|
|
||||||
*/
|
|
||||||
@interface OSCProfile : NSObject <NSCoding, NSSecureCoding>
|
|
||||||
|
|
||||||
@property NSString *name;
|
|
||||||
@property NSMutableArray <OnScreenButtonState *> *buttonStates;
|
|
||||||
@property BOOL isSelected;
|
|
||||||
|
|
||||||
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected;
|
|
||||||
|
|
||||||
+ (BOOL) supportsSecureCoding;
|
|
||||||
- (id) initWithCoder:(NSCoder*)decoder;
|
|
||||||
- (void) encodeWithCoder:(NSCoder*)encoder;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
//
|
|
||||||
// OSCProfile.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Long Le on 12/22/22.
|
|
||||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "OSCProfile.h"
|
|
||||||
|
|
||||||
@implementation OSCProfile
|
|
||||||
|
|
||||||
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected {
|
|
||||||
if ((self = [self init])) {
|
|
||||||
self.name = name;
|
|
||||||
self.buttonStates = buttonStates;
|
|
||||||
self.isSelected = isSelected;
|
|
||||||
}
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (BOOL) supportsSecureCoding {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) encodeWithCoder:(NSCoder*)encoder {
|
|
||||||
[encoder encodeObject:self.name forKey:@"name"];
|
|
||||||
[encoder encodeObject:self.buttonStates forKey:@"buttonStates"];
|
|
||||||
[encoder encodeBool:self.isSelected forKey:@"isSelected"];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id) initWithCoder:(NSCoder*)decoder {
|
|
||||||
if (self = [super init]) {
|
|
||||||
self.name = [decoder decodeObjectForKey:@"name"];
|
|
||||||
self.buttonStates = [decoder decodeObjectForKey:@"buttonStates"];
|
|
||||||
self.isSelected = [decoder decodeBoolForKey:@"isSelected"];
|
|
||||||
}
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
//
|
|
||||||
// OSCProfilesManager.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Long Le on 1/1/23.
|
|
||||||
// Copyright © 2023 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "OSCProfile.h"
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
/**
|
|
||||||
This singleton object can be accessed from any class and provides methods to get and set on screen controller profile related data.
|
|
||||||
Note that the implementation file contains a number of 'Helper' methods. These helper methods are only used in this class's implementation file and help to reduce re-writing large blocks of code that are called multiple times throughout the file
|
|
||||||
*/
|
|
||||||
@interface OSCProfilesManager : NSObject
|
|
||||||
|
|
||||||
|
|
||||||
+ (OSCProfilesManager *) sharedManager;
|
|
||||||
+ (void) setOnScreenWidgetViewsSet:(NSMutableSet* )set;
|
|
||||||
|
|
||||||
#pragma mark - Getters
|
|
||||||
/**
|
|
||||||
* Returns an array of decoded profile objects
|
|
||||||
*/
|
|
||||||
- (NSMutableArray *) getAllProfiles;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the OSC Profile that is currently selected to be displayed on screen during game streaming
|
|
||||||
*/
|
|
||||||
- (OSCProfile *) getSelectedProfile;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the index of the 'selected' profile within the array it's in
|
|
||||||
*/
|
|
||||||
- (NSInteger) getIndexOfSelectedProfile;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Setters
|
|
||||||
/**
|
|
||||||
* Sets the profile object with the particular 'name' as the selected profile to be displayed on screen during game streaming
|
|
||||||
*/
|
|
||||||
- (void) setProfileToSelected:(NSString *)name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
- (bool) updateSelectedProfile:(NSMutableArray *) oscButtonLayers;
|
|
||||||
/**
|
|
||||||
* Delete current selected profile.
|
|
||||||
*/
|
|
||||||
- (void) deleteCurrentSelectedProfile;
|
|
||||||
|
|
||||||
#pragma mark - Queries
|
|
||||||
/**
|
|
||||||
* Lets the caller of this method know whether a profile with a given name already exists in persistent storage
|
|
||||||
*/
|
|
||||||
- (BOOL) profileNameAlreadyExist:(NSString*)name;
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
//
|
|
||||||
// OSCProfilesManager.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Long Le on 1/1/23.
|
|
||||||
// Copyright © 2023 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "OSCProfilesManager.h"
|
|
||||||
#import "OnScreenButtonState.h"
|
|
||||||
#import "Moonlight-Swift.h"
|
|
||||||
#import "LayoutOnScreenControlsViewController.h"
|
|
||||||
#import "OnScreenControls.h"
|
|
||||||
|
|
||||||
@implementation OSCProfilesManager
|
|
||||||
|
|
||||||
static NSMutableSet *OnScreenWidgetViews;
|
|
||||||
|
|
||||||
#pragma mark - Initializer
|
|
||||||
|
|
||||||
+ (OSCProfilesManager *) sharedManager {
|
|
||||||
static OSCProfilesManager *_sharedManager = nil;
|
|
||||||
static dispatch_once_t onceToken;
|
|
||||||
|
|
||||||
dispatch_once(&onceToken, ^{
|
|
||||||
_sharedManager = [[self alloc] init];
|
|
||||||
});
|
|
||||||
return _sharedManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Class Helper Methods
|
|
||||||
|
|
||||||
|
|
||||||
+ (void) setOnScreenWidgetViewsSet:(NSMutableSet* )set{
|
|
||||||
OnScreenWidgetViews = set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the profile whose 'name' property has a value that is equal to the 'name' passed into the method
|
|
||||||
*/
|
|
||||||
- (OSCProfile *) OSCProfileWithName:(NSString*)name {
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
|
|
||||||
if ([profile.name isEqualToString:name]) {
|
|
||||||
return profile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of encoded 'OSCProfile' objects from the array of decoded 'OSCProfile' objects passed into this method
|
|
||||||
*/
|
|
||||||
- (NSMutableArray *) encodedProfilesFromArray:(NSMutableArray *)profiles {
|
|
||||||
|
|
||||||
NSMutableArray *profilesEncoded = [[NSMutableArray alloc] init];
|
|
||||||
/* encode each profile and add them back into an array */
|
|
||||||
for (OSCProfile *profile in profiles) { //
|
|
||||||
NSData *profileEncoded = [NSKeyedArchiver archivedDataWithRootObject:profile requiringSecureCoding:YES error:nil];
|
|
||||||
[profilesEncoded addObject:profileEncoded];
|
|
||||||
}
|
|
||||||
|
|
||||||
return profilesEncoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an 'OSCProfile' object of specific index for another in the 'OSCProfile' objects array stored in persistent storage
|
|
||||||
*/
|
|
||||||
-(void)deleteCurrentSelectedProfile{
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
NSInteger selectedIndex = [self getIndexOfSelectedProfile];
|
|
||||||
|
|
||||||
/* Remove the selected profile */
|
|
||||||
if(selectedIndex != 0) [profiles removeObjectAtIndex:selectedIndex]; //deleting default profile (index 0) is not allowed
|
|
||||||
else {
|
|
||||||
NSLog(@"Default profile!"); // to be updated here.
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedIndex--;
|
|
||||||
if(selectedIndex < 0) selectedIndex = 0;
|
|
||||||
OSCProfile *newSelectedProfile = [profiles objectAtIndex:selectedIndex];
|
|
||||||
newSelectedProfile.isSelected = YES;
|
|
||||||
|
|
||||||
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 in the array, which are already encoded. Save array to persistent storage */
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
- (void) replaceProfile:(OSCProfile*)oldProfile withProfile:(OSCProfile*)newProfile {
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
|
|
||||||
for (OSCProfile *profile in profiles) { // set all profiles' 'isSelected' property to NO since the new profile we're saving over the old profile with will be the 'selected' profile
|
|
||||||
profile.isSelected = NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Set the new profile as the selected one. The reasoning behind this is that this method is currently being used when the user saves over an existing profile with another profile that has the same name. The expected behavior is that the newly saved profile becomes the selected profile which will show on screen when they launch the game stream view */
|
|
||||||
newProfile.isSelected = YES;
|
|
||||||
|
|
||||||
/* Remove the old profile from the array and insert the new profile into its place */
|
|
||||||
int index = 0;
|
|
||||||
for (int i = 0; i < profiles.count; i++) {
|
|
||||||
|
|
||||||
if ([[profiles[i] name] isEqualToString: oldProfile.name]) {
|
|
||||||
index = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[profiles removeObjectAtIndex:index];
|
|
||||||
[profiles insertObject:newProfile atIndex:index];
|
|
||||||
|
|
||||||
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 in the array, which are already encoded. Save array to persistent storage */
|
|
||||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
|
||||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Globally Accessible Methods
|
|
||||||
|
|
||||||
#pragma mark - Getters
|
|
||||||
|
|
||||||
- (NSMutableArray *) getAllProfiles {
|
|
||||||
|
|
||||||
NSData *profilesArrayEncoded = [[NSUserDefaults standardUserDefaults] objectForKey: @"OSCProfiles"]; // Get the encoded array of encoded OSC profiles from persistent storage
|
|
||||||
NSSet *classes = [NSSet setWithObjects:[NSString class], [NSMutableData class], [NSMutableArray class], [OSCProfile class], [OnScreenButtonState class], nil];
|
|
||||||
|
|
||||||
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 */
|
|
||||||
NSMutableArray *profilesDecoded = [[NSMutableArray alloc] init];
|
|
||||||
OSCProfile *profileDecoded;
|
|
||||||
for (NSData *profileEncoded in profilesEncoded) {
|
|
||||||
|
|
||||||
profileDecoded = [NSKeyedUnarchiver unarchivedObjectOfClasses: classes fromData:profileEncoded error: nil];
|
|
||||||
[profilesDecoded addObject: profileDecoded];
|
|
||||||
}
|
|
||||||
return profilesDecoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (OSCProfile *) getSelectedProfile {
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
|
|
||||||
if (profile.isSelected) {
|
|
||||||
return profile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSInteger) getIndexOfSelectedProfile {
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
|
|
||||||
if (profile.isSelected == YES) {
|
|
||||||
return [profiles indexOfObject: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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Setters
|
|
||||||
|
|
||||||
- (void) setProfileToSelected:(NSString *)name {
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
|
|
||||||
/* Iterate through each profile. If its name equals the value of the 'name' parameter passed into this method then set the profile's 'isSelected' property to YES, otherwise set the value to NO */
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
|
|
||||||
if ([profile.name isEqualToString:name]) {
|
|
||||||
profile.isSelected = YES;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
profile.isSelected = NO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 */
|
|
||||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
|
||||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (bool) updateSelectedProfile:(NSMutableArray *) oscButtonLayers {
|
|
||||||
NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndButtonsToButtonStates:oscButtonLayers];
|
|
||||||
if([self getIndexOfSelectedProfile]==0) return false;
|
|
||||||
OSCProfile *newProfile = [[OSCProfile alloc] initWithName:[self getSelectedProfile].name
|
|
||||||
buttonStates:buttonStatesEncoded isSelected:YES]; // create a new 'OSCProfile'. Set the array of encoded button states created above to the 'buttonStates' property of the new profile, along with a 'name'. Set 'isSelected' argument to YES which will set this saved profile as the one that will show up in the game stream view
|
|
||||||
|
|
||||||
|
|
||||||
/* set all saved OSCProfiles 'isSelected' property to NO since the new profile you're adding will be set as the selected profile */
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
profile.isSelected = NO;
|
|
||||||
}
|
|
||||||
[self replaceProfile:[self getSelectedProfile] withProfile:newProfile];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void) saveProfileWithName:(NSString*)name andButtonLayers:(NSMutableArray *)oscButtonLayers {
|
|
||||||
NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndButtonsToButtonStates:oscButtonLayers];
|
|
||||||
OSCProfile *newProfile = [[OSCProfile alloc] initWithName:name
|
|
||||||
buttonStates:buttonStatesEncoded isSelected:YES]; // create a new 'OSCProfile'. Set the array of encoded button states created above to the 'buttonStates' property of the new profile, along with a 'name'. Set 'isSelected' argument to YES which will set this saved profile as the one that will show up in the game stream view
|
|
||||||
/* set all saved OSCProfiles 'isSelected' property to NO since the new profile you're adding will be set as the selected profile */
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
|
|
||||||
profile.isSelected = NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([self profileNameAlreadyExist:name]) { // if a saved profile with the same 'name' already exists in persistent storage then overwrite it and save the change to persistent storage
|
|
||||||
[self replaceProfile:[self OSCProfileWithName:name] withProfile:newProfile];
|
|
||||||
}
|
|
||||||
else { // otherwise encode then add the new profile to the end of the OSCProfiles array
|
|
||||||
NSData *newProfileEncoded = [NSKeyedArchiver archivedDataWithRootObject:newProfile requiringSecureCoding:YES error:nil];
|
|
||||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles];
|
|
||||||
[profilesEncoded addObject:newProfileEncoded];
|
|
||||||
|
|
||||||
/* Encode the 'profilesEncoded' array itself, NOT the objects in the 'profilesEncoded' array, all of which are already encoded by this point */
|
|
||||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
|
||||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
||||||
|
|
||||||
// saving test:
|
|
||||||
/*
|
|
||||||
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
|
|
||||||
NSString *path = [documentsPath stringByAppendingPathComponent:@"newDefault.bin"];
|
|
||||||
[newProfileEncoded writeToFile:path atomically:YES];
|
|
||||||
NSLog(@"默认 Profile 数据已保存到: %@", path);
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (NSMutableArray* ) convertOnScreenControllerAndButtonsToButtonStates:(NSMutableArray *) oscButtonLayers {
|
|
||||||
/* iterate through each OSC button the user sees on screen, create an 'OnScreenButtonState' object from each button, encode each object, and then add each object to an array */
|
|
||||||
/*
|
|
||||||
NSSet *validPositionButtonNames = [NSSet setWithObjects:
|
|
||||||
@"l2Button",
|
|
||||||
@"l1Button",
|
|
||||||
@"dPad",
|
|
||||||
@"selectButton",
|
|
||||||
@"leftStickBackground",
|
|
||||||
@"rightStickBackground",
|
|
||||||
@"r2Button",
|
|
||||||
@"r1Button",
|
|
||||||
@"aButton",
|
|
||||||
@"bButton",
|
|
||||||
@"xButton",
|
|
||||||
@"yButton",
|
|
||||||
@"startButton",
|
|
||||||
nil]; */
|
|
||||||
NSMutableArray *buttonStatesEncoded = [[NSMutableArray alloc] init];
|
|
||||||
|
|
||||||
// save on-screen game controller buttons & sticks as buttonstate:
|
|
||||||
for (CALayer *oscButtonLayer in oscButtonLayers) {
|
|
||||||
|
|
||||||
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:oscButtonLayer.name buttonType:LegacyOscButton andPosition:oscButtonLayer.position];
|
|
||||||
// add hidden attr here
|
|
||||||
buttonState.isHidden = oscButtonLayer.isHidden;
|
|
||||||
buttonState.oscLayerSizeFactor = [OnScreenControls getControllerLayerSizeFactor:oscButtonLayer];
|
|
||||||
buttonState.backgroundAlpha = oscButtonLayer.opacity;
|
|
||||||
NSLog(@"oscLayerName: %@, opacity: %f, ", oscButtonLayer.name, buttonState.backgroundAlpha);
|
|
||||||
|
|
||||||
|
|
||||||
// buttonState.oscLayerSizeFactor = oscButtonLayer.bounds;
|
|
||||||
|
|
||||||
NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
|
|
||||||
[buttonStatesEncoded addObject: buttonStateEncoded];
|
|
||||||
}
|
|
||||||
|
|
||||||
// save on-screen widget views (keyboard & mouse command) as buttonstate:
|
|
||||||
for(OnScreenWidgetView* widgetView in OnScreenWidgetViews){
|
|
||||||
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:widgetView.keyString buttonType:CustomOnScreenWidget andPosition:widgetView.center];
|
|
||||||
buttonState.alias = widgetView.keyLabel;
|
|
||||||
buttonState.timestamp = widgetView.timestamp;
|
|
||||||
buttonState.widthFactor = widgetView.widthFactor;
|
|
||||||
buttonState.heightFactor = widgetView.heightFactor;
|
|
||||||
buttonState.backgroundAlpha = widgetView.backgroundAlpha;
|
|
||||||
buttonState.borderWidth = widgetView.borderWidth;
|
|
||||||
buttonState.sensitivityFactor = widgetView.sensitivityFactor;
|
|
||||||
buttonState.stickIndicatorOffset = widgetView.stickIndicatorOffset;
|
|
||||||
|
|
||||||
NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
|
|
||||||
[buttonStatesEncoded addObject: buttonStateEncoded];
|
|
||||||
|
|
||||||
}
|
|
||||||
return buttonStatesEncoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Queries
|
|
||||||
|
|
||||||
- (BOOL) profileNameAlreadyExist:(NSString*)name {
|
|
||||||
NSMutableArray *profiles = [self getAllProfiles];
|
|
||||||
|
|
||||||
/* Iterate through the decoded profiles and return 'YES' if one of the profiles' 'name' properties equals the 'name' passed into this method */
|
|
||||||
for (OSCProfile *profile in profiles) {
|
|
||||||
|
|
||||||
if ([profile.name isEqualToString:name]) {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
//
|
|
||||||
// OnScreenButtonState.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Long Le on 10/20/22.
|
|
||||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
/**
|
|
||||||
This object is used to save positional and visibility information for any particular on screen virtual controller button.
|
|
||||||
We are able to associate this 'OnScreenButtonState' object and its corresponding CALayer onscreen controller button by setting their 'name' properties equal; in our particular case we give them descriptive names such as 'aButton', 'upButton', 'leftStick', etc. By keeping references to the 19 on screen controller buttons (CALayers) in an array, and creating 19 'OnScreenButtonState' objects with names corresponding to these 19 CALayers and keeping them in an array, we can iterate through both arrays to find OSC buttons (CALayers) with the same name as one of the 'OnScreenButtonStateObjects' and then set the CALayer on screen control button's 'position' and 'hidden' property according to the value of the 'OnScreenButtonState' objects 'position' and 'isHidden' properties.
|
|
||||||
Naturally we would like the user to be able to save their controller layout configurations so that they can load them between app launches, so we adopt encoding/decoding related protocols so we encode these 'OnScreenButtonState' objects and save them to NSUserDefaults
|
|
||||||
*/
|
|
||||||
|
|
||||||
// OS Button state info obj to be modified
|
|
||||||
@interface OnScreenButtonState : NSObject <NSCoding, NSSecureCoding>
|
|
||||||
|
|
||||||
@property NSString *name;
|
|
||||||
@property NSString *alias;
|
|
||||||
@property CGPoint position;
|
|
||||||
@property (nonatomic, assign) BOOL isHidden;
|
|
||||||
@property (nonatomic, assign) uint8_t buttonType;
|
|
||||||
@property (nonatomic, assign) NSTimeInterval timestamp;
|
|
||||||
@property (nonatomic, assign) CGFloat widthFactor; // for OnScreenWidgetView
|
|
||||||
@property (nonatomic, assign) CGFloat heightFactor; // for OnScreenWidgetView
|
|
||||||
@property (nonatomic, assign) CGFloat borderWidth; // for OnScreenWidgetView
|
|
||||||
@property (nonatomic, assign) CGFloat sensitivityFactor; // for OnScreenWidgetView
|
|
||||||
@property (nonatomic, assign) CGFloat stickIndicatorOffset; // for OnScreenWidgetView
|
|
||||||
|
|
||||||
@property (nonatomic, assign) CGFloat oscLayerSizeFactor; // for OnScreenController CALayer
|
|
||||||
@property (nonatomic, assign) CGFloat backgroundAlpha; // for OnScreenController CALayer
|
|
||||||
|
|
||||||
// @property (nonatomic, assign) BOOL hasValidPosition;
|
|
||||||
|
|
||||||
typedef NS_ENUM(NSInteger, OnScreenButtonType) {
|
|
||||||
LegacyOscButton,
|
|
||||||
CustomOnScreenWidget
|
|
||||||
};
|
|
||||||
|
|
||||||
- (id) initWithButtonName:(NSString*)name buttonType:(uint8_t)buttonType andPosition:(CGPoint)position;
|
|
||||||
|
|
||||||
+ (BOOL) supportsSecureCoding;
|
|
||||||
- (void) encodeWithCoder:(NSCoder*)encoder;
|
|
||||||
- (id) initWithCoder:(NSCoder*)decoder;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
//
|
|
||||||
// OnScreenButtonState.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Long Le on 10/20/22.
|
|
||||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "OnScreenButtonState.h"
|
|
||||||
|
|
||||||
@implementation OnScreenButtonState
|
|
||||||
|
|
||||||
|
|
||||||
- (id) initWithButtonName:(NSString*)name buttonType:(uint8_t)buttonType andPosition:(CGPoint)position {
|
|
||||||
if ((self = [self init])) {
|
|
||||||
self.name = name;
|
|
||||||
//self.isHidden = isHidden;
|
|
||||||
self.position = position;
|
|
||||||
self.buttonType = buttonType;
|
|
||||||
}
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (BOOL) supportsSecureCoding {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) encodeWithCoder:(NSCoder*)encoder {
|
|
||||||
[encoder encodeObject:self.name forKey:@"name"];
|
|
||||||
[encoder encodeObject:self.alias forKey:@"alias"];
|
|
||||||
[encoder encodeDouble:self.timestamp forKey:@"timestamp"];
|
|
||||||
[encoder encodeInt:self.buttonType forKey:@"buttonType"];
|
|
||||||
[encoder encodeCGPoint:self.position forKey:@"position"];
|
|
||||||
[encoder encodeBool:self.isHidden forKey:@"isHidden"];
|
|
||||||
[encoder encodeFloat:self.widthFactor forKey:@"widthFactor"];
|
|
||||||
[encoder encodeFloat:self.heightFactor forKey:@"heightFactor"];
|
|
||||||
[encoder encodeFloat:self.sensitivityFactor forKey:@"velocityFactor"];
|
|
||||||
[encoder encodeFloat:self.stickIndicatorOffset forKey:@"stickIndicatorOffset"];
|
|
||||||
[encoder encodeFloat:self.oscLayerSizeFactor forKey:@"oscLayerSizeFactor"];
|
|
||||||
[encoder encodeFloat:self.backgroundAlpha forKey:@"backgroundAlpha"];
|
|
||||||
[encoder encodeFloat:self.borderWidth forKey:@"borderWidth"];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id) initWithCoder:(NSCoder*)decoder {
|
|
||||||
if (self = [super init]) {
|
|
||||||
self.name = [decoder decodeObjectForKey:@"name"];
|
|
||||||
self.alias = [decoder decodeObjectForKey:@"alias"];
|
|
||||||
self.timestamp = [decoder decodeDoubleForKey:@"timestamp"];
|
|
||||||
self.buttonType = [decoder decodeIntForKey:@"buttonType"];
|
|
||||||
self.position = [decoder decodeCGPointForKey:@"position"];
|
|
||||||
self.isHidden = [decoder decodeBoolForKey:@"isHidden"];
|
|
||||||
self.widthFactor = [decoder decodeFloatForKey:@"widthFactor"];
|
|
||||||
self.heightFactor = [decoder decodeFloatForKey:@"heightFactor"];
|
|
||||||
self.sensitivityFactor = [decoder decodeFloatForKey:@"velocityFactor"];
|
|
||||||
self.stickIndicatorOffset = [decoder decodeFloatForKey:@"stickIndicatorOffset"];
|
|
||||||
self.oscLayerSizeFactor = [decoder decodeFloatForKey:@"oscLayerSizeFactor"];
|
|
||||||
self.backgroundAlpha = [decoder decodeFloatForKey:@"backgroundAlpha"];
|
|
||||||
self.borderWidth = [decoder decodeFloatForKey:@"borderWidth"];
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
//
|
|
||||||
// HapticContext.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Cameron Gutman on 9/17/20.
|
|
||||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "HapticContext.h"
|
|
||||||
|
|
||||||
@import CoreHaptics;
|
|
||||||
@import GameController;
|
|
||||||
|
|
||||||
@implementation HapticContext {
|
|
||||||
GCControllerPlayerIndex _playerIndex;
|
|
||||||
CHHapticEngine* _hapticEngine API_AVAILABLE(ios(13.0), tvos(14.0));
|
|
||||||
id<CHHapticPatternPlayer> _hapticPlayer API_AVAILABLE(ios(13.0), tvos(14.0));
|
|
||||||
BOOL _playing;
|
|
||||||
}
|
|
||||||
|
|
||||||
-(void)cleanup API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
|
||||||
if (_hapticPlayer != nil) {
|
|
||||||
[_hapticPlayer cancelAndReturnError:nil];
|
|
||||||
_hapticPlayer = nil;
|
|
||||||
}
|
|
||||||
if (_hapticEngine != nil) {
|
|
||||||
[_hapticEngine stopWithCompletionHandler:nil];
|
|
||||||
_hapticEngine = nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-(void)setMotorAmplitude:(unsigned short)amplitude API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
|
||||||
NSError* error;
|
|
||||||
|
|
||||||
// Check if the haptic engine died
|
|
||||||
if (_hapticEngine == nil) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the effect entirely if the amplitude is 0
|
|
||||||
if (amplitude == 0) {
|
|
||||||
if (_playing) {
|
|
||||||
[_hapticPlayer stopAtTime:0 error:&error];
|
|
||||||
_playing = NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_hapticPlayer == nil) {
|
|
||||||
// We must initialize the intensity to 1.0f because the dynamic parameters are multiplied by this value before being applied
|
|
||||||
CHHapticEventParameter* intensityParameter = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f];
|
|
||||||
CHHapticEvent* hapticEvent = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObject:intensityParameter] relativeTime:0 duration:GCHapticDurationInfinite];
|
|
||||||
CHHapticPattern* hapticPattern = [[CHHapticPattern alloc] initWithEvents:[NSArray arrayWithObject:hapticEvent] parameters:[[NSArray alloc] init] error:&error];
|
|
||||||
if (error != nil) {
|
|
||||||
Log(LOG_W, @"Controller %d: Haptic pattern creation failed: %@", _playerIndex, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_hapticPlayer = [_hapticEngine createPlayerWithPattern:hapticPattern error:&error];
|
|
||||||
if (error != nil) {
|
|
||||||
Log(LOG_W, @"Controller %d: Haptic player creation failed: %@", _playerIndex, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CHHapticDynamicParameter* intensityParameter = [[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl value:amplitude / 65535.0f relativeTime:0];
|
|
||||||
[_hapticPlayer sendParameters:[NSArray arrayWithObject:intensityParameter] atTime:CHHapticTimeImmediate error:&error];
|
|
||||||
if (error != nil) {
|
|
||||||
Log(LOG_W, @"Controller %d: Haptic player parameter update failed: %@", _playerIndex, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_playing) {
|
|
||||||
[_hapticPlayer startAtTime:0 error:&error];
|
|
||||||
if (error != nil) {
|
|
||||||
_hapticPlayer = nil;
|
|
||||||
Log(LOG_W, @"Controller %d: Haptic playback start failed: %@", _playerIndex, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_playing = YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-(id) initWithGamepad:(GCController*)gamepad locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
|
||||||
if (gamepad.haptics == nil) {
|
|
||||||
Log(LOG_W, @"Controller %d does not support haptics", gamepad.playerIndex);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (![[gamepad.haptics supportedLocalities] containsObject:locality]) {
|
|
||||||
Log(LOG_W, @"Controller %d does not support haptic locality: %@", gamepad.playerIndex, locality);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
_playerIndex = gamepad.playerIndex;
|
|
||||||
_hapticEngine = [gamepad.haptics createEngineWithLocality:locality];
|
|
||||||
|
|
||||||
NSError* error;
|
|
||||||
[_hapticEngine startAndReturnError:&error];
|
|
||||||
if (error != nil) {
|
|
||||||
Log(LOG_W, @"Controller %d: Haptic engine failed to start: %@", gamepad.playerIndex, error);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad {
|
|
||||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
|
||||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightHandle];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+(HapticContext*) createContextForLowFreqMotor:(GCController*)gamepad {
|
|
||||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
|
||||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftHandle];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+(HapticContext*) createContextForLeftTrigger:(GCController*)gamepad {
|
|
||||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
|
||||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftTrigger];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+(HapticContext*) createContextForRightTrigger:(GCController*)gamepad {
|
|
||||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
|
||||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightTrigger];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
//
|
|
||||||
// NativeTouchHandler.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by ZWM on 2024/6/16.
|
|
||||||
// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "NativeTouchHandler.h"
|
|
||||||
#import "NativeTouchPointer.h"
|
|
||||||
#import "OnScreenControls.h"
|
|
||||||
#import "StreamView.h"
|
|
||||||
|
|
||||||
#include <Limelight.h>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@implementation NativeTouchHandler {
|
|
||||||
StreamView* streamView;
|
|
||||||
TemporarySettings* currentSettings;
|
|
||||||
bool activateCoordSelector;
|
|
||||||
bool touchPointSpawnedAtUpperScreenEdge;
|
|
||||||
|
|
||||||
// Use a Dictionary to store UITouch object's memory address as key, and pointerId as value,字典存放UITouch对象地址和pointerId映射关系
|
|
||||||
// pointerId will be generated from a pre-defined pool
|
|
||||||
// Use a NSSet store active pointerId,
|
|
||||||
NSMutableDictionary *pointerIdDict; //pointerId Dict for active touches.
|
|
||||||
NSMutableSet<NSNumber *> *activePointerIds; //pointerId Set for active touches.
|
|
||||||
NSMutableSet<NSNumber *> *pointerIdPool; //pre-defined pool of pointerIds.
|
|
||||||
NSMutableSet<NSNumber *> *unassignedPointerIds;
|
|
||||||
|
|
||||||
CGFloat slideGestureVerticalThreshold;
|
|
||||||
CGFloat screenWidthWithThreshold;
|
|
||||||
CGFloat EDGE_TOLERANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings{
|
|
||||||
self = [super init];
|
|
||||||
self->streamView = view;
|
|
||||||
self->currentSettings = settings;
|
|
||||||
self->activateCoordSelector = currentSettings.pointerVelocityModeDivider.floatValue != 1.0;
|
|
||||||
// self->notPureNativeTouchMode = !(currentSettings.touchMode.intValue == PURE_NATIVE_TOUCH);
|
|
||||||
|
|
||||||
self->pointerIdDict = [NSMutableDictionary dictionary];
|
|
||||||
self->pointerIdPool = [NSMutableSet set];
|
|
||||||
for (uint8_t i = 0; i <= 10; i++) { //ipadOS supports upto 11 finger touches
|
|
||||||
[self->pointerIdPool addObject:@(i)];
|
|
||||||
}
|
|
||||||
self->activePointerIds = [NSMutableSet set];
|
|
||||||
// self->excludedPointerIds = [[NSMutableSet alloc] init];
|
|
||||||
self->touchPointSpawnedAtUpperScreenEdge = false;
|
|
||||||
|
|
||||||
EDGE_TOLERANCE = 15.0;
|
|
||||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
|
||||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
|
||||||
|
|
||||||
[NativeTouchPointer setPointerVelocityDivider:settings.pointerVelocityModeDivider.floatValue];
|
|
||||||
[NativeTouchPointer setPointerVelocityFactor:settings.touchPointerVelocityFactor.floatValue];
|
|
||||||
[NativeTouchPointer initContextWithView:self->streamView];
|
|
||||||
//_touchesCapturedByOnScreenButtons = [[NSMutableSet alloc] init];
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// generate & populate pointerId into NSDict & NSSet, called in touchesBegan
|
|
||||||
- (void)handleTouchDown:(UITouch*)touch{
|
|
||||||
//populate pointerId
|
|
||||||
uintptr_t memAddrValue = (uintptr_t)touch;
|
|
||||||
unassignedPointerIds = [pointerIdPool mutableCopy]; //reset unassignedPointerIds
|
|
||||||
[unassignedPointerIds minusSet:activePointerIds];
|
|
||||||
uint8_t pointerId = [[unassignedPointerIds anyObject] unsignedIntValue];
|
|
||||||
[pointerIdDict setObject:@(pointerId) forKey:@(memAddrValue)];
|
|
||||||
[activePointerIds addObject:@(pointerId)];
|
|
||||||
|
|
||||||
//check if touch point is spawned on the left or right upper half screen edges, event to remote PC. this is for better handling in-stream slide gesture
|
|
||||||
CGPoint initialPoint = [touch locationInView:self->streamView];
|
|
||||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
|
||||||
self->touchPointSpawnedAtUpperScreenEdge = true;
|
|
||||||
// [excludedPointerIds addObject:@(pointerId)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove pointerId in touchesEnded or touchesCancelled
|
|
||||||
- (void)removePointerId:(UITouch*)touch{
|
|
||||||
uintptr_t memAddrValue = (uintptr_t)touch;
|
|
||||||
NSNumber* pointerIdObj = [pointerIdDict objectForKey:@(memAddrValue)];
|
|
||||||
if(pointerIdObj != nil){
|
|
||||||
[activePointerIds removeObject:pointerIdObj];
|
|
||||||
[pointerIdDict removeObjectForKey:@(memAddrValue)];
|
|
||||||
// if([excludedPointerIds containsObject:pointerIdObj]) [excludedPointerIds removeObject:pointerIdObj]; // remove pointer id from excludedPointerId NSSet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从字典中获取UITouch事件对应的pointerId
|
|
||||||
// called in method of sendTouchEvent
|
|
||||||
- (uint8_t) retrievePointerIdFromDict:(UITouch*)touch{
|
|
||||||
return [[pointerIdDict objectForKey:@((uintptr_t)touch)] unsignedIntValue];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType{
|
|
||||||
if(touchPointSpawnedAtUpperScreenEdge) 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!
|
|
||||||
|
|
||||||
CGPoint targetCoords;
|
|
||||||
// NSLog(@"excluded count: %d", (uint32_t)[excludedPointerIds count]);
|
|
||||||
if(activateCoordSelector && touch.phase == UITouchPhaseMoved) targetCoords = [NativeTouchPointer selectCoordsFor:touch]; // coordinates of touch pointer replaced to relative ones here.
|
|
||||||
else targetCoords = [touch locationInView:streamView];
|
|
||||||
|
|
||||||
CGPoint location = [streamView adjustCoordinatesForVideoArea:targetCoords];
|
|
||||||
CGSize videoSize = [streamView getVideoAreaSize];
|
|
||||||
CGFloat normalizedX = location.x / videoSize.width;
|
|
||||||
CGFloat normalizedY = location.y / videoSize.height;
|
|
||||||
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
|
||||||
|
|
||||||
if([NativeTouchPointer getPointerObjFromDict:touch].needResetCoords){ // access whether the current pointer has reached the boundary, and need a coord reset.
|
|
||||||
LiSendTouchEvent(LI_TOUCH_EVENT_UP, pointerId, normalizedX, normalizedY, 0, 0, 0, 0); //event must sent from the lowest level directy by LiSendTouchEvent to simulate continous dragging to another point on screen
|
|
||||||
LiSendTouchEvent(LI_TOUCH_EVENT_DOWN, pointerId, 0.3, 0.4, 0, 0, 0, 0);
|
|
||||||
}else LiSendTouchEvent(touchType, pointerId, normalizedX, normalizedY,(touch.force / touch.maximumPossibleForce) / sin(touch.altitudeAngle),0.0f, 0.0f,[streamView getRotationFromAzimuthAngle:[touch azimuthAngleInView:streamView]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
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(activateCoordSelector) [NativeTouchPointer populatePointerObjIntoDict:touch];
|
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
// NSLog(@"captured by OSB touches: %d", (uint32_t)[OnScreenControls.touchAddrsCapturedByOnScreenControls count]);
|
|
||||||
for (UITouch* touch in touches){
|
|
||||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
|
||||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
|
||||||
if(activateCoordSelector) [NativeTouchPointer updatePointerObjInDict:touch];
|
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
|
||||||
[[NativeTouchPointer getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
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(activateCoordSelector) [NativeTouchPointer removePointerObjFromDict:touch];
|
|
||||||
}
|
|
||||||
if(touchPointSpawnedAtUpperScreenEdge && [[event allTouches] count] == [touches count]) touchPointSpawnedAtUpperScreenEdge = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
[self touchesEnded:touches withEvent:event];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
//
|
|
||||||
// NativeTouchPointer.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by ZWM on 2024/5/14.
|
|
||||||
// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "StreamView.h"
|
|
||||||
#import "NativeTouchHandler.h"
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
@interface NativeTouchPointer : NSObject
|
|
||||||
@property (nonatomic, assign) bool needResetCoords;
|
|
||||||
|
|
||||||
+ (void)initContextWithView:(StreamView *)view;
|
|
||||||
+ (void)setPointerVelocityDivider:(CGFloat)dividerLocation;
|
|
||||||
+ (void)setPointerVelocityFactor:(CGFloat)velocityFactor;
|
|
||||||
|
|
||||||
+ (NativeTouchPointer* )getPointerObjFromDict:(UITouch*)touch;
|
|
||||||
- (bool)doesNeedResetCoords;
|
|
||||||
|
|
||||||
+ (void)populatePointerObjIntoDict:(UITouch*)touch;
|
|
||||||
+ (void)removePointerObjFromDict:(UITouch*)touch;
|
|
||||||
+ (void)updatePointerObjInDict:(UITouch *)touch;
|
|
||||||
+ (CGPoint)selectCoordsFor:(UITouch *)touch;
|
|
||||||
|
|
||||||
|
|
||||||
- (instancetype)initWithTouch:(UITouch *)touch;
|
|
||||||
@end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
//
|
|
||||||
// NativeTouchPointer.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by ZWM on 2024/5/14.
|
|
||||||
// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "NativeTouchPointer.h"
|
|
||||||
#include <Limelight.h>
|
|
||||||
|
|
||||||
// native touch pointer ojbect that stores & manipulates touch coordinates
|
|
||||||
|
|
||||||
static NSMutableDictionary *pointerObjDict;
|
|
||||||
|
|
||||||
static CGFloat pointerVelocityFactor;
|
|
||||||
static CGFloat pointerVelocityDivider;
|
|
||||||
static CGFloat pointerVelocityDividerLocationByPoints;
|
|
||||||
static CGFloat streamViewHeight;
|
|
||||||
static CGFloat streamViewWidth;
|
|
||||||
static CGFloat fixedResetCoordX;
|
|
||||||
static CGFloat fixedResetCoordY;
|
|
||||||
static StreamView* _streamView;
|
|
||||||
|
|
||||||
@implementation NativeTouchPointer{
|
|
||||||
CGPoint initialPoint;
|
|
||||||
CGPoint latestPoint;
|
|
||||||
CGPoint previousPoint;
|
|
||||||
CGPoint latestRelativePoint;
|
|
||||||
CGPoint previousRelativePoint;
|
|
||||||
bool useRelativeCoords;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (instancetype)initWithTouch:(UITouch *)touch{
|
|
||||||
self = [super init];
|
|
||||||
self->initialPoint = [touch locationInView:_streamView];
|
|
||||||
self->latestPoint = self->initialPoint;
|
|
||||||
self->latestRelativePoint = self->initialPoint;
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (bool)doesNeedResetCoords{
|
|
||||||
bool boundaryReached = (latestRelativePoint.x > streamViewWidth || latestRelativePoint.x < 0.0f || latestRelativePoint.y > streamViewHeight || latestRelativePoint.y < 0.0f);
|
|
||||||
bool withinExcludedArea = (initialPoint.x > streamViewWidth * (0.75) && initialPoint.x < streamViewWidth) && (initialPoint.y > streamViewHeight * (0.8) && initialPoint.y < streamViewHeight);
|
|
||||||
_needResetCoords = (pointerVelocityFactor > 1.0f) && self->useRelativeCoords && boundaryReached && !withinExcludedArea;
|
|
||||||
// boundary detection & coordinates reset to the specific point for HK:StarTrail(needs a very high pointer velocity)
|
|
||||||
// must exclude touch pointer that uses native coords instead of relative ones.
|
|
||||||
// also exclude touch pointer created within the bottom right corner
|
|
||||||
|
|
||||||
return _needResetCoords;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)updatePointerCoords:(UITouch *)touch{
|
|
||||||
previousPoint = latestPoint;
|
|
||||||
latestPoint = [touch locationInView:_streamView];
|
|
||||||
previousRelativePoint = latestRelativePoint;
|
|
||||||
if(_needResetCoords){// boundary detection & coordinates reset to the central screen point for HK:StarTrail(needs a very high pointer velocity); // boundary detection & coordinates reset to specific point for HK:StarTrail(needs a very high pointer velocity)
|
|
||||||
previousRelativePoint.x = fixedResetCoordX;
|
|
||||||
previousRelativePoint.y = fixedResetCoordY;
|
|
||||||
}
|
|
||||||
latestRelativePoint.x = previousRelativePoint.x + pointerVelocityFactor * (latestPoint.x - previousPoint.x);
|
|
||||||
latestRelativePoint.y = previousRelativePoint.y + pointerVelocityFactor * (latestPoint.y - previousPoint.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (void)initContextWithView:(StreamView *)view {
|
|
||||||
_streamView = view;
|
|
||||||
streamViewWidth = _streamView.frame.size.width;
|
|
||||||
fixedResetCoordX = streamViewWidth * 0.3;
|
|
||||||
streamViewHeight = _streamView.frame.size.height;
|
|
||||||
fixedResetCoordY = streamViewHeight * 0.4;
|
|
||||||
NSLog(@"stream wdith %f, stream height %f", streamViewWidth, streamViewHeight);
|
|
||||||
pointerObjDict = [NSMutableDictionary dictionary];
|
|
||||||
pointerVelocityDividerLocationByPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]) * pointerVelocityDivider;
|
|
||||||
NSLog(@"pointerVelocityDivider: %.2f", pointerVelocityDivider);
|
|
||||||
NSLog(@"pointerVelocityFactor: %.2f", pointerVelocityFactor);
|
|
||||||
NSLog(@"pointerVelocityDividerLocationByPoints: %.2f", pointerVelocityDividerLocationByPoints);
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (void)setPointerVelocityDivider:(CGFloat)dividerLocation{
|
|
||||||
pointerVelocityDivider = dividerLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (void)setPointerVelocityFactor:(CGFloat)velocityFactor{
|
|
||||||
pointerVelocityFactor = velocityFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
+ (void)populatePointerObjIntoDict:(UITouch*)touch{
|
|
||||||
[pointerObjDict setObject:[[NativeTouchPointer alloc] initWithTouch:touch] forKey:@((uintptr_t)touch)];
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (void)removePointerObjFromDict:(UITouch*)touch{
|
|
||||||
uintptr_t memAddrValue = (uintptr_t)touch;
|
|
||||||
NativeTouchPointer* pointer = [pointerObjDict objectForKey:@(memAddrValue)];
|
|
||||||
if(pointer != nil){
|
|
||||||
[pointerObjDict removeObjectForKey:@(memAddrValue)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (NativeTouchPointer* )getPointerObjFromDict:(UITouch*)touch{
|
|
||||||
return [pointerObjDict objectForKey:@((uintptr_t)touch)];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
+ (void)updatePointerObjInDict:(UITouch *)touch{
|
|
||||||
[[pointerObjDict objectForKey:@((uintptr_t)touch)] updatePointerCoords:touch];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
+ (CGPoint)selectCoordsFor:(UITouch *)touch{
|
|
||||||
NativeTouchPointer *pointer = [pointerObjDict objectForKey:@((uintptr_t)touch)];
|
|
||||||
if(pointer == nil) return CGPointMake(0, 0);
|
|
||||||
if((pointer -> initialPoint).x > pointerVelocityDividerLocationByPoints){ //if first contact coords locates on the right side of divider.
|
|
||||||
pointer -> useRelativeCoords = true;
|
|
||||||
return pointer->latestRelativePoint;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
pointer -> useRelativeCoords = false;
|
|
||||||
return[touch locationInView:_streamView];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
//
|
|
||||||
// NativeTouchHandler.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by ZWM on 2024/6/16.
|
|
||||||
// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
|
|
||||||
#import "StreamView.h"
|
|
||||||
#import "DataManager.h"
|
|
||||||
#include <Limelight.h>
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
@interface PureNativeTouchHandler : UIResponder
|
|
||||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
//
|
|
||||||
// NativeTouchHandler.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by ZWM on 2024/8/29.
|
|
||||||
// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "PureNativeTouchHandler.h"
|
|
||||||
#import "NativeTouchPointer.h"
|
|
||||||
#import "StreamView.h"
|
|
||||||
|
|
||||||
#include <Limelight.h>
|
|
||||||
|
|
||||||
|
|
||||||
@implementation PureNativeTouchHandler {
|
|
||||||
StreamView* streamView;
|
|
||||||
TemporarySettings* currentSettings;
|
|
||||||
bool activateCoordSelector;
|
|
||||||
bool touchPointSpawnedAtUpperScreenEdge;
|
|
||||||
|
|
||||||
// Use a Dictionary to store UITouch object's memory address as key, and pointerId as value,字典存放UITouch对象地址和pointerId映射关系
|
|
||||||
// pointerId will be generated from a pre-defined pool
|
|
||||||
// Use a NSSet store active pointerId,
|
|
||||||
NSMutableDictionary *pointerIdDict; //pointerId Dict for active touches.
|
|
||||||
NSMutableSet<NSNumber *> *activePointerIds; //pointerId Set for active touches.
|
|
||||||
NSMutableSet<NSNumber *> *pointerIdPool; //pre-defined pool of pointerIds.
|
|
||||||
NSMutableSet<NSNumber *> *unassignedPointerIds;
|
|
||||||
|
|
||||||
CGFloat slideGestureVerticalThreshold;
|
|
||||||
CGFloat screenWidthWithThreshold;
|
|
||||||
CGFloat EDGE_TOLERANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings{
|
|
||||||
self = [super init];
|
|
||||||
self->streamView = view;
|
|
||||||
self->currentSettings = settings;
|
|
||||||
self->activateCoordSelector = currentSettings.pointerVelocityModeDivider.floatValue != 1.0;
|
|
||||||
|
|
||||||
pointerIdDict = [NSMutableDictionary dictionary];
|
|
||||||
pointerIdPool = [NSMutableSet set];
|
|
||||||
for (uint8_t i = 0; i <= 10; i++) { //ipadOS supports upto 11 finger touches
|
|
||||||
[pointerIdPool addObject:@(i)];
|
|
||||||
}
|
|
||||||
activePointerIds = [NSMutableSet set];
|
|
||||||
self->touchPointSpawnedAtUpperScreenEdge = false;
|
|
||||||
|
|
||||||
EDGE_TOLERANCE = 15.0;
|
|
||||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
|
||||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
|
||||||
|
|
||||||
[NativeTouchPointer setPointerVelocityDivider:settings.pointerVelocityModeDivider.floatValue];
|
|
||||||
[NativeTouchPointer setPointerVelocityFactor:settings.touchPointerVelocityFactor.floatValue];
|
|
||||||
[NativeTouchPointer initContextWithView:self->streamView];
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// generate & populate pointerId into NSDict & NSSet, called in touchesBegan
|
|
||||||
- (void)handleTouchDown:(UITouch*)touch{
|
|
||||||
uintptr_t memAddrValue = (uintptr_t)touch;
|
|
||||||
unassignedPointerIds = [pointerIdPool mutableCopy]; //reset unassignedPointerIds
|
|
||||||
[unassignedPointerIds minusSet:activePointerIds];
|
|
||||||
uint8_t pointerId = [[unassignedPointerIds anyObject] unsignedIntValue];
|
|
||||||
[pointerIdDict setObject:@(pointerId) forKey:@(memAddrValue)];
|
|
||||||
[activePointerIds addObject:@(pointerId)];
|
|
||||||
|
|
||||||
//check if touch point is spawned on the left or right upper half screen edges,
|
|
||||||
CGPoint initialPoint = [touch locationInView:self->streamView];
|
|
||||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
|
||||||
self->touchPointSpawnedAtUpperScreenEdge = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove pointerId in touchesEnded or touchesCancelled
|
|
||||||
- (void)removePointerId:(UITouch*)touch{
|
|
||||||
uintptr_t memAddrValue = (uintptr_t)touch;
|
|
||||||
NSNumber* pointerIdObj = [pointerIdDict objectForKey:@(memAddrValue)];
|
|
||||||
if(pointerIdObj != nil){
|
|
||||||
[activePointerIds removeObject:pointerIdObj];
|
|
||||||
[pointerIdDict removeObjectForKey:@(memAddrValue)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从字典中获取UITouch事件对应的pointerId
|
|
||||||
// called in method of sendTouchEvent
|
|
||||||
- (uint8_t) retrievePointerIdFromDict:(UITouch*)touch{
|
|
||||||
return [[pointerIdDict objectForKey:@((uintptr_t)touch)] unsignedIntValue];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType{
|
|
||||||
if(touchPointSpawnedAtUpperScreenEdge) 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!
|
|
||||||
|
|
||||||
CGPoint targetCoords;
|
|
||||||
if(activateCoordSelector && touch.phase == UITouchPhaseMoved) targetCoords = [NativeTouchPointer selectCoordsFor:touch]; // coordinates of touch pointer replaced to relative ones here.
|
|
||||||
else targetCoords = [touch locationInView:streamView];
|
|
||||||
|
|
||||||
CGPoint location = [streamView adjustCoordinatesForVideoArea:targetCoords];
|
|
||||||
CGSize videoSize = [streamView getVideoAreaSize];
|
|
||||||
CGFloat normalizedX = location.x / videoSize.width;
|
|
||||||
CGFloat normalizedY = location.y / videoSize.height;
|
|
||||||
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
|
||||||
|
|
||||||
if([NativeTouchPointer getPointerObjFromDict:touch].needResetCoords){ // access whether the current pointer has reached the boundary, and need a coord reset.
|
|
||||||
LiSendTouchEvent(LI_TOUCH_EVENT_UP, pointerId, normalizedX, normalizedY, 0, 0, 0, 0); //event must sent from the lowest level directy by LiSendTouchEvent to simulate continous dragging to another point on screen
|
|
||||||
LiSendTouchEvent(LI_TOUCH_EVENT_DOWN, pointerId, 0.3, 0.4, 0, 0, 0, 0);
|
|
||||||
}else LiSendTouchEvent(touchType, pointerId, normalizedX, normalizedY,(touch.force / touch.maximumPossibleForce) / sin(touch.altitudeAngle),0.0f, 0.0f,[streamView getRotationFromAzimuthAngle:[touch azimuthAngleInView:streamView]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
for (UITouch* touch in touches){
|
|
||||||
[self handleTouchDown:touch]; //generate & populate pointerId
|
|
||||||
if(activateCoordSelector) [NativeTouchPointer populatePointerObjIntoDict:touch];
|
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
for (UITouch* touch in touches){
|
|
||||||
if(activateCoordSelector) [NativeTouchPointer updatePointerObjInDict:touch];
|
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
|
||||||
[[NativeTouchPointer getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
for (UITouch* touch in touches){
|
|
||||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
|
||||||
[self removePointerId:touch]; //then remove pointerId
|
|
||||||
if(activateCoordSelector) [NativeTouchPointer removePointerObjFromDict:touch];
|
|
||||||
}
|
|
||||||
if(touchPointSpawnedAtUpperScreenEdge && [[event allTouches] count] == [touches count]) touchPointSpawnedAtUpperScreenEdge = false;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
[self touchesEnded:touches withEvent:event];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
//
|
|
||||||
// StreamView.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Cameron Gutman on 10/19/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ControllerSupport.h"
|
|
||||||
#import "OnScreenControls.h"
|
|
||||||
#import "Moonlight-Swift.h"
|
|
||||||
#import "StreamConfiguration.h"
|
|
||||||
|
|
||||||
@protocol UserInteractionDelegate <NSObject>
|
|
||||||
|
|
||||||
- (void) userInteractionBegan;
|
|
||||||
- (void) userInteractionEnded;
|
|
||||||
- (void) streamExitRequested;
|
|
||||||
- (void) toggleStatsOverlay;
|
|
||||||
- (void) localMousePointerMode;
|
|
||||||
- (void) toggleMouseVisible;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
|
||||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate>
|
|
||||||
#else
|
|
||||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@property (assign, nonatomic) UIView* streamFrameTopLayerView;
|
|
||||||
|
|
||||||
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
|
||||||
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
|
|
||||||
config:(StreamConfiguration*)streamConfig
|
|
||||||
streamFrameTopLayerView:(UIView* )topLayerView
|
|
||||||
;
|
|
||||||
- (void) showOnScreenControls;
|
|
||||||
- (void) setOnScreenControls;
|
|
||||||
- (void) disableOnScreenControls;
|
|
||||||
- (void) reloadOnScreenControlsRealtimeWith:(ControllerSupport*)controllerSupport
|
|
||||||
andConfig:(StreamConfiguration*)streamConfig;
|
|
||||||
- (void) reloadOnScreenControlsWith:(ControllerSupport*)controllerSupport
|
|
||||||
andConfig:(StreamConfiguration*)streamConfig;
|
|
||||||
- (void) clearOnScreenKeyboardButtons;
|
|
||||||
- (void) reloadOnScreenWidgetViews;
|
|
||||||
|
|
||||||
- (CGSize) getVideoAreaSize;
|
|
||||||
- (CGPoint) adjustCoordinatesForVideoArea:(CGPoint)point;
|
|
||||||
- (uint16_t)getRotationFromAzimuthAngle:(float)azimuthAngle;
|
|
||||||
|
|
||||||
- (OnScreenControlsLevel) getCurrentOscState;
|
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="6254" systemVersion="14C81f" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES">
|
|
||||||
<attribute name="address" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" syncable="YES"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES">
|
|
||||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" syncable="YES"/>
|
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" syncable="YES"/>
|
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" syncable="YES"/>
|
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="2" syncable="YES"/>
|
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<elements>
|
|
||||||
<element name="Host" positionX="0" positionY="0" width="128" height="148"/>
|
|
||||||
<element name="Settings" positionX="0" positionY="0" width="128" height="120"/>
|
|
||||||
</elements>
|
|
||||||
</model>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="6254" systemVersion="14C81f" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES">
|
|
||||||
<attribute name="address" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" syncable="YES"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES">
|
|
||||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" syncable="YES"/>
|
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" syncable="YES"/>
|
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" syncable="YES"/>
|
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
|
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<elements>
|
|
||||||
<element name="Host" positionX="0" positionY="0" width="128" height="148"/>
|
|
||||||
<element name="Settings" positionX="0" positionY="0" width="128" height="120"/>
|
|
||||||
</elements>
|
|
||||||
</model>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="13772" systemVersion="17D102" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
|
||||||
<entity name="App" representedClassName="App" syncable="YES">
|
|
||||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="image" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
|
|
||||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES">
|
|
||||||
<attribute name="address" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES">
|
|
||||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="streamingRemotely" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<elements>
|
|
||||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
|
||||||
<element name="Host" positionX="0" positionY="0" width="128" height="163"/>
|
|
||||||
<element name="Settings" positionX="0" positionY="0" width="128" height="150"/>
|
|
||||||
</elements>
|
|
||||||
</model>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="9057" systemVersion="15B42" minimumToolsVersion="Automatic">
|
|
||||||
<entity name="App" representedClassName="App" syncable="YES">
|
|
||||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="image" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
|
|
||||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES">
|
|
||||||
<attribute name="address" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" syncable="YES"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES">
|
|
||||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" syncable="YES"/>
|
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" syncable="YES"/>
|
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" syncable="YES"/>
|
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
|
|
||||||
<attribute name="uniqueId" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<elements>
|
|
||||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
|
||||||
<element name="Host" positionX="0" positionY="0" width="128" height="163"/>
|
|
||||||
<element name="Settings" positionX="0" positionY="0" width="128" height="135"/>
|
|
||||||
</elements>
|
|
||||||
</model>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14135" systemVersion="17F77" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
|
||||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="image" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<elements>
|
|
||||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
|
||||||
<element name="Host" positionX="0" positionY="0" width="128" height="165"/>
|
|
||||||
<element name="Settings" positionX="0" positionY="0" width="128" height="225"/>
|
|
||||||
</elements>
|
|
||||||
</model>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="23H420" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
|
||||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
|
||||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" 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="deviceGyroMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="enableHdr" 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="0" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" 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="largerStickLR1" 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="mousePointerVelocityFactor" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="oscLayoutToolFingers" optional="YES" attributeType="Integer 16" defaultValueString="4" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="oscTapExlusionAreaSize" optional="YES" attributeType="Float" defaultValueString="1.03" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="pointerVelocityModeDivider" optional="YES" attributeType="Float" defaultValueString="0.5" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="preferredCodec" attributeType="Integer 32" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="resolutionSelected" optional="YES" attributeType="Integer 16" defaultValueString="5" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="reverseMouseWheelDirection" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="showKeyboardToolbar" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="slideToSettingsDistance" optional="YES" attributeType="Float" defaultValueString="0.5" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="slideToSettingsScreenEdge" optional="YES" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="statsOverlayEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="statsOverlayLevel" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="touchMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="touchPointerVelocityFactor" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="unlockDisplayOrientation" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
</model>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14135" systemVersion="17G65" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
|
||||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<elements>
|
|
||||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
|
||||||
<element name="Host" positionX="0" positionY="0" width="128" height="165"/>
|
|
||||||
<element name="Settings" positionX="0" positionY="0" width="128" height="225"/>
|
|
||||||
</elements>
|
|
||||||
</model>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18C54" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
|
||||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
|
||||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<elements>
|
|
||||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
|
||||||
<element name="Host" positionX="0" positionY="0" width="128" height="195"/>
|
|
||||||
<element name="Settings" positionX="0" positionY="0" width="128" height="225"/>
|
|
||||||
</elements>
|
|
||||||
</model>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14490.99" systemVersion="18F132" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
|
||||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
|
||||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<elements>
|
|
||||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
|
||||||
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
|
|
||||||
<element name="Settings" positionX="0" positionY="0" width="128" height="225"/>
|
|
||||||
</elements>
|
|
||||||
</model>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19E266" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
|
||||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
|
||||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<elements>
|
|
||||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
|
||||||
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
|
|
||||||
<element name="Settings" positionX="0" positionY="0" width="128" height="238"/>
|
|
||||||
</elements>
|
|
||||||
</model>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="22G313" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
|
||||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
|
||||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" 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="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="statsOverlayEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
</model>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="22G313" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
|
||||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
|
||||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" 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="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="statsOverlayEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
</model>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="22G313" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
|
||||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
|
||||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" 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="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="statsOverlayEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
</model>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="22G313" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
|
||||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
|
||||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
|
||||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
|
||||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" 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="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="statsOverlayEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
|
||||||
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="useHevc2" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
|
||||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
</model>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
//
|
|
||||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include <Limelight.h>
|
|
||||||
#import "OnScreenControls.h"
|
|
||||||
// #import "RelativeTouchHandler.h"
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
//
|
|
||||||
// Connection.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 1/19/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "VideoDecoderRenderer.h"
|
|
||||||
#import "StreamConfiguration.h"
|
|
||||||
|
|
||||||
#define CONN_TEST_SERVER "ios.conntest.moonlight-stream.org"
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
CFTimeInterval startTime;
|
|
||||||
CFTimeInterval endTime;
|
|
||||||
int totalFrames;
|
|
||||||
int receivedFrames;
|
|
||||||
int networkDroppedFrames;
|
|
||||||
int totalHostProcessingLatency;
|
|
||||||
int framesWithHostProcessingLatency;
|
|
||||||
int maxHostProcessingLatency;
|
|
||||||
int minHostProcessingLatency;
|
|
||||||
} video_stats_t;
|
|
||||||
|
|
||||||
@interface Connection : NSOperation <NSStreamDelegate>
|
|
||||||
|
|
||||||
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks;
|
|
||||||
-(void) terminate;
|
|
||||||
-(void) main;
|
|
||||||
-(BOOL) getVideoStats:(video_stats_t*)stats;
|
|
||||||
-(NSString*) getActiveCodecName;
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,508 +0,0 @@
|
|||||||
//
|
|
||||||
// Connection.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 1/19/14.
|
|
||||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "Connection.h"
|
|
||||||
#import "Utils.h"
|
|
||||||
|
|
||||||
#import <VideoToolbox/VideoToolbox.h>
|
|
||||||
|
|
||||||
#define SDL_MAIN_HANDLED
|
|
||||||
#import <SDL.h>
|
|
||||||
|
|
||||||
#include "Limelight.h"
|
|
||||||
#include "opus_multistream.h"
|
|
||||||
|
|
||||||
@implementation Connection {
|
|
||||||
SERVER_INFORMATION _serverInfo;
|
|
||||||
STREAM_CONFIGURATION _streamConfig;
|
|
||||||
CONNECTION_LISTENER_CALLBACKS _clCallbacks;
|
|
||||||
DECODER_RENDERER_CALLBACKS _drCallbacks;
|
|
||||||
AUDIO_RENDERER_CALLBACKS _arCallbacks;
|
|
||||||
char _hostString[256];
|
|
||||||
char _appVersionString[32];
|
|
||||||
char _gfeVersionString[32];
|
|
||||||
char _rtspSessionUrl[128];
|
|
||||||
}
|
|
||||||
|
|
||||||
static NSLock* initLock;
|
|
||||||
static OpusMSDecoder* opusDecoder;
|
|
||||||
static id<ConnectionCallbacks> _callbacks;
|
|
||||||
static int lastFrameNumber;
|
|
||||||
static int activeVideoFormat;
|
|
||||||
static video_stats_t currentVideoStats;
|
|
||||||
static video_stats_t lastVideoStats;
|
|
||||||
static NSLock* videoStatsLock;
|
|
||||||
|
|
||||||
static SDL_AudioDeviceID audioDevice;
|
|
||||||
static OPUS_MULTISTREAM_CONFIGURATION audioConfig;
|
|
||||||
static void* audioBuffer;
|
|
||||||
static int audioFrameSize;
|
|
||||||
|
|
||||||
static VideoDecoderRenderer* renderer;
|
|
||||||
|
|
||||||
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
|
|
||||||
{
|
|
||||||
[renderer setupWithVideoFormat:videoFormat width:width height:height frameRate:redrawRate];
|
|
||||||
lastFrameNumber = 0;
|
|
||||||
activeVideoFormat = videoFormat;
|
|
||||||
memset(¤tVideoStats, 0, sizeof(currentVideoStats));
|
|
||||||
memset(&lastVideoStats, 0, sizeof(lastVideoStats));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DrStart(void)
|
|
||||||
{
|
|
||||||
[renderer start];
|
|
||||||
}
|
|
||||||
|
|
||||||
void DrStop(void)
|
|
||||||
{
|
|
||||||
[renderer stop];
|
|
||||||
}
|
|
||||||
|
|
||||||
-(BOOL) getVideoStats:(video_stats_t*)stats
|
|
||||||
{
|
|
||||||
// We return lastVideoStats because it is a complete 1 second window
|
|
||||||
[videoStatsLock lock];
|
|
||||||
if (lastVideoStats.endTime != 0) {
|
|
||||||
memcpy(stats, &lastVideoStats, sizeof(*stats));
|
|
||||||
[videoStatsLock unlock];
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No stats yet
|
|
||||||
[videoStatsLock unlock];
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
-(NSString*) getActiveCodecName
|
|
||||||
{
|
|
||||||
switch (activeVideoFormat)
|
|
||||||
{
|
|
||||||
case VIDEO_FORMAT_H264:
|
|
||||||
return @"H.264";
|
|
||||||
case VIDEO_FORMAT_H264_HIGH8_444:
|
|
||||||
return @"H.264 YUV444";
|
|
||||||
case VIDEO_FORMAT_H265:
|
|
||||||
return @"HEVC";
|
|
||||||
case VIDEO_FORMAT_H265_REXT8_444:
|
|
||||||
return @"HEVC YUV444";
|
|
||||||
case VIDEO_FORMAT_H265_MAIN10:
|
|
||||||
if (LiGetCurrentHostDisplayHdrMode()) {
|
|
||||||
return @"HEVC Main 10 HDR";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return @"HEVC Main 10 SDR";
|
|
||||||
}
|
|
||||||
case VIDEO_FORMAT_H265_REXT10_444:
|
|
||||||
if (LiGetCurrentHostDisplayHdrMode()) {
|
|
||||||
return @"HEVC Main 10 YUV444 HDR";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return @"HEVC Main 10 YUV444 SDR";
|
|
||||||
}
|
|
||||||
case VIDEO_FORMAT_AV1_MAIN8:
|
|
||||||
return @"AV1";
|
|
||||||
case VIDEO_FORMAT_AV1_HIGH8_444:
|
|
||||||
return @"AV1 YUV444";
|
|
||||||
case VIDEO_FORMAT_AV1_MAIN10:
|
|
||||||
if (LiGetCurrentHostDisplayHdrMode()) {
|
|
||||||
return @"AV1 10-bit HDR";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return @"AV1 10-bit SDR";
|
|
||||||
}
|
|
||||||
case VIDEO_FORMAT_AV1_HIGH10_444:
|
|
||||||
if (LiGetCurrentHostDisplayHdrMode()) {
|
|
||||||
return @"AV1 10-bit YUV444 HDR";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return @"AV1 10-bit YUV444 SDR";
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return @"UNKNOWN";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
|
|
||||||
{
|
|
||||||
int offset = 0;
|
|
||||||
int ret;
|
|
||||||
unsigned char* data = (unsigned char*) malloc(decodeUnit->fullLength);
|
|
||||||
if (data == NULL) {
|
|
||||||
// A frame was lost due to OOM condition
|
|
||||||
return DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
CFTimeInterval now = CACurrentMediaTime();
|
|
||||||
if (!lastFrameNumber) {
|
|
||||||
currentVideoStats.startTime = now;
|
|
||||||
lastFrameNumber = decodeUnit->frameNumber;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Flip stats roughly every second
|
|
||||||
if (now - currentVideoStats.startTime >= 1.0f) {
|
|
||||||
currentVideoStats.endTime = now;
|
|
||||||
|
|
||||||
[videoStatsLock lock];
|
|
||||||
lastVideoStats = currentVideoStats;
|
|
||||||
[videoStatsLock unlock];
|
|
||||||
|
|
||||||
memset(¤tVideoStats, 0, sizeof(currentVideoStats));
|
|
||||||
currentVideoStats.startTime = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame
|
|
||||||
currentVideoStats.networkDroppedFrames += decodeUnit->frameNumber - (lastFrameNumber + 1);
|
|
||||||
currentVideoStats.totalFrames += decodeUnit->frameNumber - (lastFrameNumber + 1);
|
|
||||||
lastFrameNumber = decodeUnit->frameNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decodeUnit->frameHostProcessingLatency != 0) {
|
|
||||||
if (currentVideoStats.minHostProcessingLatency == 0 || decodeUnit->frameHostProcessingLatency < currentVideoStats.minHostProcessingLatency) {
|
|
||||||
currentVideoStats.minHostProcessingLatency = decodeUnit->frameHostProcessingLatency;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decodeUnit->frameHostProcessingLatency > currentVideoStats.maxHostProcessingLatency) {
|
|
||||||
currentVideoStats.maxHostProcessingLatency = decodeUnit->frameHostProcessingLatency;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentVideoStats.framesWithHostProcessingLatency++;
|
|
||||||
currentVideoStats.totalHostProcessingLatency += decodeUnit->frameHostProcessingLatency;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentVideoStats.receivedFrames++;
|
|
||||||
currentVideoStats.totalFrames++;
|
|
||||||
|
|
||||||
PLENTRY entry = decodeUnit->bufferList;
|
|
||||||
while (entry != NULL) {
|
|
||||||
// Submit parameter set NALUs directly since no copy is required by the decoder
|
|
||||||
if (entry->bufferType != BUFFER_TYPE_PICDATA) {
|
|
||||||
ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data
|
|
||||||
length:entry->length
|
|
||||||
bufferType:entry->bufferType
|
|
||||||
decodeUnit:decodeUnit];
|
|
||||||
if (ret != DR_OK) {
|
|
||||||
free(data);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
memcpy(&data[offset], entry->data, entry->length);
|
|
||||||
offset += entry->length;
|
|
||||||
}
|
|
||||||
|
|
||||||
entry = entry->next;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function will take our picture data buffer
|
|
||||||
return [renderer submitDecodeBuffer:data
|
|
||||||
length:offset
|
|
||||||
bufferType:BUFFER_TYPE_PICDATA
|
|
||||||
decodeUnit:decodeUnit];
|
|
||||||
}
|
|
||||||
|
|
||||||
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags)
|
|
||||||
{
|
|
||||||
int err;
|
|
||||||
SDL_AudioSpec want, have;
|
|
||||||
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
|
|
||||||
Log(LOG_E, @"Failed to initialize audio subsystem: %s\n", SDL_GetError());
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_zero(want);
|
|
||||||
want.freq = opusConfig->sampleRate;
|
|
||||||
want.format = AUDIO_S16;
|
|
||||||
want.channels = opusConfig->channelCount;
|
|
||||||
want.samples = opusConfig->samplesPerFrame;
|
|
||||||
|
|
||||||
audioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
|
||||||
if (audioDevice == 0) {
|
|
||||||
Log(LOG_E, @"Failed to open audio device: %s\n", SDL_GetError());
|
|
||||||
ArCleanup();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
audioConfig = *opusConfig;
|
|
||||||
audioFrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
|
|
||||||
audioBuffer = SDL_malloc(audioFrameSize);
|
|
||||||
if (audioBuffer == NULL) {
|
|
||||||
Log(LOG_E, @"Failed to allocate audio frame buffer");
|
|
||||||
ArCleanup();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
opusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
|
|
||||||
opusConfig->channelCount,
|
|
||||||
opusConfig->streams,
|
|
||||||
opusConfig->coupledStreams,
|
|
||||||
opusConfig->mapping,
|
|
||||||
&err);
|
|
||||||
if (opusDecoder == NULL) {
|
|
||||||
Log(LOG_E, @"Failed to create Opus decoder");
|
|
||||||
ArCleanup();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start playback
|
|
||||||
SDL_PauseAudioDevice(audioDevice, 0);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ArCleanup(void)
|
|
||||||
{
|
|
||||||
if (opusDecoder != NULL) {
|
|
||||||
opus_multistream_decoder_destroy(opusDecoder);
|
|
||||||
opusDecoder = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audioDevice != 0) {
|
|
||||||
SDL_CloseAudioDevice(audioDevice);
|
|
||||||
audioDevice = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audioBuffer != NULL) {
|
|
||||||
SDL_free(audioBuffer);
|
|
||||||
audioBuffer = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
|
|
||||||
{
|
|
||||||
int decodeLen;
|
|
||||||
|
|
||||||
// Don't queue if there's already more than 30 ms of audio data waiting
|
|
||||||
// in Moonlight's audio queue.
|
|
||||||
if (LiGetPendingAudioDuration() > 30) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
decodeLen = opus_multistream_decode(opusDecoder, (unsigned char *)sampleData, sampleLength,
|
|
||||||
(short*)audioBuffer, audioConfig.samplesPerFrame, 0);
|
|
||||||
if (decodeLen > 0) {
|
|
||||||
// Provide backpressure on the queue to ensure too many frames don't build up
|
|
||||||
// in SDL's audio queue.
|
|
||||||
while (SDL_GetQueuedAudioSize(audioDevice) / audioFrameSize > 10) {
|
|
||||||
SDL_Delay(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SDL_QueueAudio(audioDevice,
|
|
||||||
audioBuffer,
|
|
||||||
sizeof(short) * decodeLen * audioConfig.channelCount) < 0) {
|
|
||||||
Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClStageStarting(int stage)
|
|
||||||
{
|
|
||||||
[_callbacks stageStarting:LiGetStageName(stage)];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClStageComplete(int stage)
|
|
||||||
{
|
|
||||||
[_callbacks stageComplete:LiGetStageName(stage)];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClStageFailed(int stage, int errorCode)
|
|
||||||
{
|
|
||||||
[_callbacks stageFailed:LiGetStageName(stage) withError:errorCode portTestFlags:LiGetPortFlagsFromStage(stage)];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClConnectionStarted(void)
|
|
||||||
{
|
|
||||||
[_callbacks connectionStarted];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClConnectionTerminated(int errorCode)
|
|
||||||
{
|
|
||||||
[_callbacks connectionTerminated: errorCode];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClLogMessage(const char* format, ...)
|
|
||||||
{
|
|
||||||
va_list va;
|
|
||||||
va_start(va, format);
|
|
||||||
vfprintf(stderr, format, va);
|
|
||||||
va_end(va);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
|
|
||||||
{
|
|
||||||
[_callbacks rumble:controllerNumber lowFreqMotor:lowFreqMotor highFreqMotor:highFreqMotor];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClConnectionStatusUpdate(int status)
|
|
||||||
{
|
|
||||||
[_callbacks connectionStatusUpdate:status];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClSetHdrMode(bool enabled)
|
|
||||||
{
|
|
||||||
[renderer setHdrMode:enabled];
|
|
||||||
[_callbacks setHdrMode:enabled];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClRumbleTriggers(uint16_t controllerNumber, uint16_t leftTriggerMotor, uint16_t rightTriggerMotor)
|
|
||||||
{
|
|
||||||
[_callbacks rumbleTriggers:controllerNumber leftTrigger:leftTriggerMotor rightTrigger:rightTriggerMotor];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClSetMotionEventState(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz)
|
|
||||||
{
|
|
||||||
[_callbacks setMotionEventState:controllerNumber motionType:motionType reportRateHz:reportRateHz];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b)
|
|
||||||
{
|
|
||||||
[_callbacks setControllerLed:controllerNumber r:r g:g b:b];
|
|
||||||
}
|
|
||||||
|
|
||||||
-(void) terminate
|
|
||||||
{
|
|
||||||
// Interrupt any action blocking LiStartConnection(). This is
|
|
||||||
// thread-safe and done outside initLock on purpose, since we
|
|
||||||
// won't be able to acquire it if LiStartConnection is in
|
|
||||||
// progress.
|
|
||||||
LiInterruptConnection();
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// blocking on the caller's thread waiting to acquire initLock.
|
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
||||||
[initLock lock];
|
|
||||||
LiStopConnection();
|
|
||||||
[initLock unlock];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks
|
|
||||||
{
|
|
||||||
self = [super init];
|
|
||||||
|
|
||||||
// Use a lock to ensure that only one thread is initializing
|
|
||||||
// or deinitializing a connection at a time.
|
|
||||||
if (initLock == nil) {
|
|
||||||
initLock = [[NSLock alloc] init];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoStatsLock == nil) {
|
|
||||||
videoStatsLock = [[NSLock alloc] init];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *rawAddress = [Utils addressPortStringToAddress:config.host];
|
|
||||||
strncpy(_hostString,
|
|
||||||
[rawAddress cStringUsingEncoding:NSUTF8StringEncoding],
|
|
||||||
sizeof(_hostString) - 1);
|
|
||||||
strncpy(_appVersionString,
|
|
||||||
[config.appVersion cStringUsingEncoding:NSUTF8StringEncoding],
|
|
||||||
sizeof(_appVersionString) - 1);
|
|
||||||
if (config.gfeVersion != nil) {
|
|
||||||
strncpy(_gfeVersionString,
|
|
||||||
[config.gfeVersion cStringUsingEncoding:NSUTF8StringEncoding],
|
|
||||||
sizeof(_gfeVersionString) - 1);
|
|
||||||
}
|
|
||||||
if (config.rtspSessionUrl != nil) {
|
|
||||||
strncpy(_rtspSessionUrl,
|
|
||||||
[config.rtspSessionUrl cStringUsingEncoding:NSUTF8StringEncoding],
|
|
||||||
sizeof(_rtspSessionUrl) - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
LiInitializeServerInformation(&_serverInfo);
|
|
||||||
_serverInfo.address = _hostString;
|
|
||||||
_serverInfo.serverInfoAppVersion = _appVersionString;
|
|
||||||
if (config.gfeVersion != nil) {
|
|
||||||
_serverInfo.serverInfoGfeVersion = _gfeVersionString;
|
|
||||||
}
|
|
||||||
if (config.rtspSessionUrl != nil) {
|
|
||||||
_serverInfo.rtspSessionUrl = _rtspSessionUrl;
|
|
||||||
}
|
|
||||||
_serverInfo.serverCodecModeSupport = config.serverCodecModeSupport;
|
|
||||||
|
|
||||||
renderer = myRenderer;
|
|
||||||
_callbacks = callbacks;
|
|
||||||
|
|
||||||
LiInitializeStreamConfiguration(&_streamConfig);
|
|
||||||
_streamConfig.width = config.width;
|
|
||||||
_streamConfig.height = config.height;
|
|
||||||
_streamConfig.fps = config.frameRate;
|
|
||||||
_streamConfig.bitrate = config.bitRate;
|
|
||||||
_streamConfig.supportedVideoFormats = config.supportedVideoFormats;
|
|
||||||
_streamConfig.audioConfiguration = config.audioConfiguration;
|
|
||||||
|
|
||||||
// Since we require iOS 12 or above, we're guaranteed to be running
|
|
||||||
// on a 64-bit device with ARMv8 crypto instructions, so we don't
|
|
||||||
// need to check for that here.
|
|
||||||
_streamConfig.encryptionFlags = ENCFLG_ALL;
|
|
||||||
|
|
||||||
if ([Utils isActiveNetworkVPN]) {
|
|
||||||
// Force remote streaming mode when a VPN is connected
|
|
||||||
_streamConfig.streamingRemotely = STREAM_CFG_REMOTE;
|
|
||||||
_streamConfig.packetSize = 1024;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Detect remote streaming automatically based on the IP address of the target
|
|
||||||
_streamConfig.streamingRemotely = STREAM_CFG_AUTO;
|
|
||||||
_streamConfig.packetSize = 1392;
|
|
||||||
}
|
|
||||||
|
|
||||||
memcpy(_streamConfig.remoteInputAesKey, [config.riKey bytes], [config.riKey length]);
|
|
||||||
memset(_streamConfig.remoteInputAesIv, 0, 16);
|
|
||||||
int riKeyId = htonl(config.riKeyId);
|
|
||||||
memcpy(_streamConfig.remoteInputAesIv, &riKeyId, sizeof(riKeyId));
|
|
||||||
|
|
||||||
LiInitializeVideoCallbacks(&_drCallbacks);
|
|
||||||
_drCallbacks.setup = DrDecoderSetup;
|
|
||||||
_drCallbacks.start = DrStart;
|
|
||||||
_drCallbacks.stop = DrStop;
|
|
||||||
_drCallbacks.capabilities = CAPABILITY_PULL_RENDERER |
|
|
||||||
CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC |
|
|
||||||
CAPABILITY_REFERENCE_FRAME_INVALIDATION_AV1;
|
|
||||||
|
|
||||||
LiInitializeAudioCallbacks(&_arCallbacks);
|
|
||||||
_arCallbacks.init = ArInit;
|
|
||||||
_arCallbacks.cleanup = ArCleanup;
|
|
||||||
_arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
|
|
||||||
_arCallbacks.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
|
|
||||||
|
|
||||||
LiInitializeConnectionCallbacks(&_clCallbacks);
|
|
||||||
_clCallbacks.stageStarting = ClStageStarting;
|
|
||||||
_clCallbacks.stageComplete = ClStageComplete;
|
|
||||||
_clCallbacks.stageFailed = ClStageFailed;
|
|
||||||
_clCallbacks.connectionStarted = ClConnectionStarted;
|
|
||||||
_clCallbacks.connectionTerminated = ClConnectionTerminated;
|
|
||||||
_clCallbacks.logMessage = ClLogMessage;
|
|
||||||
_clCallbacks.rumble = ClRumble;
|
|
||||||
_clCallbacks.connectionStatusUpdate = ClConnectionStatusUpdate;
|
|
||||||
_clCallbacks.setHdrMode = ClSetHdrMode;
|
|
||||||
_clCallbacks.rumbleTriggers = ClRumbleTriggers;
|
|
||||||
_clCallbacks.setMotionEventState = ClSetMotionEventState;
|
|
||||||
_clCallbacks.setControllerLED = ClSetControllerLED;
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
-(void) main
|
|
||||||
{
|
|
||||||
[initLock lock];
|
|
||||||
LiStartConnection(&_serverInfo,
|
|
||||||
&_streamConfig,
|
|
||||||
&_clCallbacks,
|
|
||||||
&_drCallbacks,
|
|
||||||
&_arCallbacks,
|
|
||||||
NULL, 0,
|
|
||||||
NULL, 0);
|
|
||||||
[initLock unlock];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
//
|
|
||||||
// VideoDecoderRenderer.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Cameron Gutman on 10/18/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
@import AVFoundation;
|
|
||||||
|
|
||||||
#import "ConnectionCallbacks.h"
|
|
||||||
|
|
||||||
#include "Limelight.h"
|
|
||||||
|
|
||||||
@interface VideoDecoderRenderer : NSObject
|
|
||||||
|
|
||||||
- (id)initWithView:(UIView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio useFramePacing:(BOOL)useFramePacing;
|
|
||||||
|
|
||||||
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate;
|
|
||||||
- (void)start;
|
|
||||||
- (void)stop;
|
|
||||||
- (void)setHdrMode:(BOOL)enabled;
|
|
||||||
|
|
||||||
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType decodeUnit:(PDECODE_UNIT)du;
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,689 +0,0 @@
|
|||||||
//
|
|
||||||
// VideoDecoderRenderer.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Cameron Gutman on 10/18/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "VideoDecoderRenderer.h"
|
|
||||||
#import "StreamView.h"
|
|
||||||
|
|
||||||
#include <libavcodec/avcodec.h>
|
|
||||||
#include <libavcodec/cbs.h>
|
|
||||||
#include <libavcodec/cbs_av1.h>
|
|
||||||
#include <libavformat/avio.h>
|
|
||||||
#include <libavutil/mem.h>
|
|
||||||
|
|
||||||
// Private libavformat API for writing the AV1 Codec Configuration Box
|
|
||||||
extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
|
||||||
int write_seq_header);
|
|
||||||
|
|
||||||
@implementation VideoDecoderRenderer {
|
|
||||||
StreamView* _view;
|
|
||||||
id<ConnectionCallbacks> _callbacks;
|
|
||||||
float _streamAspectRatio;
|
|
||||||
|
|
||||||
AVSampleBufferDisplayLayer* displayLayer;
|
|
||||||
int videoFormat;
|
|
||||||
int frameRate;
|
|
||||||
|
|
||||||
NSMutableArray *parameterSetBuffers;
|
|
||||||
NSData *masteringDisplayColorVolume;
|
|
||||||
NSData *contentLightLevelInfo;
|
|
||||||
CMVideoFormatDescriptionRef formatDesc;
|
|
||||||
|
|
||||||
CADisplayLink* _displayLink;
|
|
||||||
BOOL framePacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)reinitializeDisplayLayer
|
|
||||||
{
|
|
||||||
CALayer *oldLayer = displayLayer;
|
|
||||||
|
|
||||||
displayLayer = [[AVSampleBufferDisplayLayer alloc] init];
|
|
||||||
displayLayer.backgroundColor = [UIColor blackColor].CGColor;
|
|
||||||
|
|
||||||
// Ensure the AVSampleBufferDisplayLayer is sized to preserve the aspect ratio
|
|
||||||
// of the video stream. We used to use AVLayerVideoGravityResizeAspect, but that
|
|
||||||
// respects the PAR encoded in the SPS which causes our computed video-relative
|
|
||||||
// touch location to be wrong in StreamView if the aspect ratio of the host
|
|
||||||
// desktop doesn't match the aspect ratio of the stream.
|
|
||||||
CGSize videoSize;
|
|
||||||
if (_view.bounds.size.width > _view.bounds.size.height * _streamAspectRatio) {
|
|
||||||
videoSize = CGSizeMake(_view.bounds.size.height * _streamAspectRatio, _view.bounds.size.height);
|
|
||||||
} else {
|
|
||||||
videoSize = CGSizeMake(_view.bounds.size.width, _view.bounds.size.width / _streamAspectRatio);
|
|
||||||
}
|
|
||||||
displayLayer.position = CGPointMake(CGRectGetMidX(_view.bounds), CGRectGetMidY(_view.bounds));
|
|
||||||
displayLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height);
|
|
||||||
displayLayer.videoGravity = AVLayerVideoGravityResize;
|
|
||||||
|
|
||||||
// Hide the layer until we get an IDR frame. This ensures we
|
|
||||||
// can see the loading progress label as the stream is starting.
|
|
||||||
displayLayer.hidden = YES;
|
|
||||||
|
|
||||||
if (oldLayer != nil) {
|
|
||||||
// Switch out the old display layer with the new one
|
|
||||||
[_view.layer replaceSublayer:oldLayer with:displayLayer];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[_view.layer addSublayer:displayLayer];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formatDesc != nil) {
|
|
||||||
CFRelease(formatDesc);
|
|
||||||
formatDesc = nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id)initWithView:(StreamView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio useFramePacing:(BOOL)useFramePacing
|
|
||||||
{
|
|
||||||
self = [super init];
|
|
||||||
|
|
||||||
_view = view;
|
|
||||||
_callbacks = callbacks;
|
|
||||||
_streamAspectRatio = aspectRatio;
|
|
||||||
framePacing = useFramePacing;
|
|
||||||
|
|
||||||
parameterSetBuffers = [[NSMutableArray alloc] init];
|
|
||||||
|
|
||||||
[self reinitializeDisplayLayer];
|
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
||||||
selector:@selector(reinitializeDisplayLayer)
|
|
||||||
name:@"ScreenChanged"
|
|
||||||
object:nil];
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate
|
|
||||||
{
|
|
||||||
self->videoFormat = videoFormat;
|
|
||||||
self->frameRate = frameRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)start
|
|
||||||
{
|
|
||||||
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
|
|
||||||
if (@available(iOS 15.0, tvOS 15.0, *)) {
|
|
||||||
_displayLink.preferredFrameRateRange = CAFrameRateRangeMake(self->frameRate, self->frameRate, self->frameRate);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
_displayLink.preferredFramesPerSecond = self->frameRate;
|
|
||||||
}
|
|
||||||
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Refactor this
|
|
||||||
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
|
||||||
|
|
||||||
- (void)displayLinkCallback:(CADisplayLink *)sender
|
|
||||||
{
|
|
||||||
VIDEO_FRAME_HANDLE handle;
|
|
||||||
PDECODE_UNIT du;
|
|
||||||
|
|
||||||
while (LiPollNextVideoFrame(&handle, &du)) {
|
|
||||||
LiCompleteVideoFrame(handle, DrSubmitDecodeUnit(du));
|
|
||||||
|
|
||||||
if (framePacing) {
|
|
||||||
// Calculate the actual display refresh rate
|
|
||||||
double displayRefreshRate = 1 / (_displayLink.targetTimestamp - _displayLink.timestamp);
|
|
||||||
|
|
||||||
// Only pace frames if the display refresh rate is >= 90% of our stream frame rate.
|
|
||||||
// Battery saver, accessibility settings, or device thermals can cause the actual
|
|
||||||
// refresh rate of the display to drop below the physical maximum.
|
|
||||||
if (displayRefreshRate >= frameRate * 0.9f) {
|
|
||||||
// Keep one pending frame to smooth out gaps due to
|
|
||||||
// network jitter at the cost of 1 frame of latency
|
|
||||||
if (LiGetPendingVideoFrames() == 1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)stop
|
|
||||||
{
|
|
||||||
[_displayLink invalidate];
|
|
||||||
}
|
|
||||||
|
|
||||||
#define NALU_START_PREFIX_SIZE 3
|
|
||||||
#define NAL_LENGTH_PREFIX_SIZE 4
|
|
||||||
|
|
||||||
- (void)updateAnnexBBufferForRange:(CMBlockBufferRef)frameBuffer dataBlock:(CMBlockBufferRef)dataBuffer offset:(int)offset length:(int)nalLength
|
|
||||||
{
|
|
||||||
OSStatus status;
|
|
||||||
size_t oldOffset = CMBlockBufferGetDataLength(frameBuffer);
|
|
||||||
|
|
||||||
// Append a 4 byte buffer to the frame block for the length prefix
|
|
||||||
status = CMBlockBufferAppendMemoryBlock(frameBuffer, NULL,
|
|
||||||
NAL_LENGTH_PREFIX_SIZE,
|
|
||||||
kCFAllocatorDefault, NULL, 0,
|
|
||||||
NAL_LENGTH_PREFIX_SIZE, 0);
|
|
||||||
if (status != noErr) {
|
|
||||||
Log(LOG_E, @"CMBlockBufferAppendMemoryBlock failed: %d", (int)status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the length prefix to the new buffer
|
|
||||||
const int dataLength = nalLength - NALU_START_PREFIX_SIZE;
|
|
||||||
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
|
|
||||||
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
|
|
||||||
status = CMBlockBufferReplaceDataBytes(lengthBytes, frameBuffer,
|
|
||||||
oldOffset, NAL_LENGTH_PREFIX_SIZE);
|
|
||||||
if (status != noErr) {
|
|
||||||
Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach the data buffer to the frame buffer by reference
|
|
||||||
status = CMBlockBufferAppendBufferReference(frameBuffer, dataBuffer, offset + NALU_START_PREFIX_SIZE, dataLength, 0);
|
|
||||||
if (status != noErr) {
|
|
||||||
Log(LOG_E, @"CMBlockBufferAppendBufferReference failed: %d", (int)status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSData*)getAv1CodecConfigurationBox:(NSData*)frameData {
|
|
||||||
AVIOContext* ioctx = NULL;
|
|
||||||
int err;
|
|
||||||
|
|
||||||
err = avio_open_dyn_buf(&ioctx);
|
|
||||||
if (err < 0) {
|
|
||||||
Log(LOG_E, @"avio_open_dyn_buf() failed: %d", err);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit the IDR frame to write the av1C blob
|
|
||||||
err = ff_isom_write_av1c(ioctx, (uint8_t*)frameData.bytes, (int)frameData.length, 1);
|
|
||||||
if (err < 0) {
|
|
||||||
Log(LOG_E, @"ff_isom_write_av1c() failed: %d", err);
|
|
||||||
// Fall-through to close and free buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the dynbuf and get the underlying buffer back (which we must free)
|
|
||||||
uint8_t* av1cBuf = NULL;
|
|
||||||
int av1cBufLen = avio_close_dyn_buf(ioctx, &av1cBuf);
|
|
||||||
|
|
||||||
Log(LOG_I, @"av1C block is %d bytes", av1cBufLen);
|
|
||||||
|
|
||||||
// Only return data if ff_isom_write_av1c() was successful
|
|
||||||
NSData* data = nil;
|
|
||||||
if (err >= 0 && av1cBufLen > 0) {
|
|
||||||
data = [NSData dataWithBytes:av1cBuf length:av1cBufLen];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
data = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
av_free(av1cBuf);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Much of this logic comes from Chrome
|
|
||||||
- (CMVideoFormatDescriptionRef)createAV1FormatDescriptionForIDRFrame:(NSData*)frameData {
|
|
||||||
NSMutableDictionary* extensions = [[NSMutableDictionary alloc] init];
|
|
||||||
|
|
||||||
CodedBitstreamContext* cbsCtx = NULL;
|
|
||||||
int err = ff_cbs_init(&cbsCtx, AV_CODEC_ID_AV1, NULL);
|
|
||||||
if (err < 0) {
|
|
||||||
Log(LOG_E, @"ff_cbs_init() failed: %d", err);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
AVPacket avPacket = {};
|
|
||||||
avPacket.data = (uint8_t*)frameData.bytes;
|
|
||||||
avPacket.size = (int)frameData.length;
|
|
||||||
|
|
||||||
// Read the sequence header OBU
|
|
||||||
CodedBitstreamFragment cbsFrag = {};
|
|
||||||
err = ff_cbs_read_packet(cbsCtx, &cbsFrag, &avPacket);
|
|
||||||
if (err < 0) {
|
|
||||||
Log(LOG_E, @"ff_cbs_read_packet() failed: %d", err);
|
|
||||||
ff_cbs_close(&cbsCtx);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
#define SET_CFSTR_EXTENSION(key, value) extensions[(__bridge NSString*)key] = (__bridge NSString*)(value)
|
|
||||||
#define SET_EXTENSION(key, value) extensions[(__bridge NSString*)key] = (value)
|
|
||||||
|
|
||||||
SET_EXTENSION(kCMFormatDescriptionExtension_FormatName, @"av01");
|
|
||||||
|
|
||||||
// We use the value for YUV without alpha, same as Chrome
|
|
||||||
// https://developer.apple.com/library/archive/qa/qa1183/_index.html
|
|
||||||
SET_EXTENSION(kCMFormatDescriptionExtension_Depth, @24);
|
|
||||||
|
|
||||||
CodedBitstreamAV1Context* bitstreamCtx = (CodedBitstreamAV1Context*)cbsCtx->priv_data;
|
|
||||||
AV1RawSequenceHeader* seqHeader = bitstreamCtx->sequence_header;
|
|
||||||
if (seqHeader == NULL) {
|
|
||||||
Log(LOG_E, @"AV1 sequence header not found in IDR frame!");
|
|
||||||
ff_cbs_fragment_free(&cbsFrag);
|
|
||||||
ff_cbs_close(&cbsCtx);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (seqHeader->color_config.color_primaries) {
|
|
||||||
case 1: // CP_BT_709
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
|
|
||||||
kCMFormatDescriptionColorPrimaries_ITU_R_709_2);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 6: // CP_BT_601
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
|
|
||||||
kCMFormatDescriptionColorPrimaries_SMPTE_C);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 9: // CP_BT_2020
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
|
|
||||||
kCMFormatDescriptionColorPrimaries_ITU_R_2020);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Log(LOG_W, @"Unsupported color_primaries value: %d", seqHeader->color_config.color_primaries);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (seqHeader->color_config.transfer_characteristics) {
|
|
||||||
case 1: // TC_BT_709
|
|
||||||
case 6: // TC_BT_601
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
|
||||||
kCMFormatDescriptionTransferFunction_ITU_R_709_2);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 7: // TC_SMPTE_240
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
|
||||||
kCMFormatDescriptionTransferFunction_SMPTE_240M_1995);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 8: // TC_LINEAR
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
|
||||||
kCMFormatDescriptionTransferFunction_Linear);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 14: // TC_BT_2020_10_BIT
|
|
||||||
case 15: // TC_BT_2020_12_BIT
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
|
||||||
kCMFormatDescriptionTransferFunction_ITU_R_2020);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 16: // TC_SMPTE_2084
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
|
||||||
kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 17: // TC_HLG
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
|
||||||
kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Log(LOG_W, @"Unsupported transfer_characteristics value: %d", seqHeader->color_config.transfer_characteristics);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (seqHeader->color_config.matrix_coefficients) {
|
|
||||||
case 1: // MC_BT_709
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
|
|
||||||
kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 6: // MC_BT_601
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
|
|
||||||
kCMFormatDescriptionYCbCrMatrix_ITU_R_601_4);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 7: // MC_SMPTE_240
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
|
|
||||||
kCMFormatDescriptionYCbCrMatrix_SMPTE_240M_1995);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 9: // MC_BT_2020_NCL
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
|
|
||||||
kCMFormatDescriptionYCbCrMatrix_ITU_R_2020);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Log(LOG_W, @"Unsupported matrix_coefficients value: %d", seqHeader->color_config.matrix_coefficients);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
SET_EXTENSION(kCMFormatDescriptionExtension_FullRangeVideo, @(seqHeader->color_config.color_range == 1));
|
|
||||||
|
|
||||||
// Progressive content
|
|
||||||
SET_EXTENSION(kCMFormatDescriptionExtension_FieldCount, @(1));
|
|
||||||
|
|
||||||
switch (seqHeader->color_config.chroma_sample_position) {
|
|
||||||
case 1: // CSP_VERTICAL
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ChromaLocationTopField,
|
|
||||||
kCMFormatDescriptionChromaLocation_Left);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2: // CSP_COLOCATED
|
|
||||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ChromaLocationTopField,
|
|
||||||
kCMFormatDescriptionChromaLocation_TopLeft);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Log(LOG_W, @"Unsupported chroma_sample_position value: %d", seqHeader->color_config.chroma_sample_position);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentLightLevelInfo) {
|
|
||||||
SET_EXTENSION(kCMFormatDescriptionExtension_ContentLightLevelInfo, contentLightLevelInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (masteringDisplayColorVolume) {
|
|
||||||
SET_EXTENSION(kCMFormatDescriptionExtension_MasteringDisplayColorVolume, masteringDisplayColorVolume);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Referenced the VP9 code in Chrome that performs a similar function
|
|
||||||
// https://source.chromium.org/chromium/chromium/src/+/main:media/gpu/mac/vt_config_util.mm;drc=977dc02c431b4979e34c7792bc3d646f649dacb4;l=155
|
|
||||||
extensions[(__bridge NSString*)kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms] =
|
|
||||||
@{
|
|
||||||
@"av1C" : [self getAv1CodecConfigurationBox:frameData],
|
|
||||||
};
|
|
||||||
extensions[@"BitsPerComponent"] = @(bitstreamCtx->bit_depth);
|
|
||||||
|
|
||||||
#undef SET_EXTENSION
|
|
||||||
#undef SET_CFSTR_EXTENSION
|
|
||||||
|
|
||||||
// AV1 doesn't have a special format description function like H.264 and HEVC have, so we just use the generic one
|
|
||||||
CMVideoFormatDescriptionRef formatDesc = NULL;
|
|
||||||
OSStatus status = CMVideoFormatDescriptionCreate(kCFAllocatorDefault, kCMVideoCodecType_AV1,
|
|
||||||
bitstreamCtx->frame_width, bitstreamCtx->frame_height,
|
|
||||||
(__bridge CFDictionaryRef)extensions,
|
|
||||||
&formatDesc);
|
|
||||||
if (status != noErr) {
|
|
||||||
Log(LOG_E, @"Failed to create AV1 format description: %d", (int)status);
|
|
||||||
formatDesc = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
ff_cbs_fragment_free(&cbsFrag);
|
|
||||||
ff_cbs_close(&cbsCtx);
|
|
||||||
return formatDesc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function must free data for bufferType == BUFFER_TYPE_PICDATA
|
|
||||||
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType decodeUnit:(PDECODE_UNIT)du
|
|
||||||
{
|
|
||||||
OSStatus status;
|
|
||||||
|
|
||||||
// Construct a new format description object each time we receive an IDR frame
|
|
||||||
if (du->frameType == FRAME_TYPE_IDR) {
|
|
||||||
if (bufferType != BUFFER_TYPE_PICDATA) {
|
|
||||||
if (bufferType == BUFFER_TYPE_VPS || bufferType == BUFFER_TYPE_SPS || bufferType == BUFFER_TYPE_PPS) {
|
|
||||||
// Add new parameter set into the parameter set array
|
|
||||||
int startLen = data[2] == 0x01 ? 3 : 4;
|
|
||||||
[parameterSetBuffers addObject:[NSData dataWithBytes:&data[startLen] length:length - startLen]];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data is NOT to be freed here. It's a direct usage of the caller's buffer.
|
|
||||||
|
|
||||||
// No frame data to submit for these NALUs
|
|
||||||
return DR_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the new format description when we get the first picture data buffer of an IDR frame.
|
|
||||||
// This is the only way we know that there is no more CSD for this frame.
|
|
||||||
//
|
|
||||||
// NB: This logic depends on the fact that we submit all picture data in one buffer!
|
|
||||||
|
|
||||||
// Free the old format description
|
|
||||||
if (formatDesc != NULL) {
|
|
||||||
CFRelease(formatDesc);
|
|
||||||
formatDesc = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
|
||||||
// Construct parameter set arrays for the format description
|
|
||||||
size_t parameterSetCount = [parameterSetBuffers count];
|
|
||||||
const uint8_t* parameterSetPointers[parameterSetCount];
|
|
||||||
size_t parameterSetSizes[parameterSetCount];
|
|
||||||
for (int i = 0; i < parameterSetCount; i++) {
|
|
||||||
NSData* parameterSet = parameterSetBuffers[i];
|
|
||||||
parameterSetPointers[i] = parameterSet.bytes;
|
|
||||||
parameterSetSizes[i] = parameterSet.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log(LOG_I, @"Constructing new H264 format description");
|
|
||||||
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
|
|
||||||
parameterSetCount,
|
|
||||||
parameterSetPointers,
|
|
||||||
parameterSetSizes,
|
|
||||||
NAL_LENGTH_PREFIX_SIZE,
|
|
||||||
&formatDesc);
|
|
||||||
if (status != noErr) {
|
|
||||||
Log(LOG_E, @"Failed to create H264 format description: %d", (int)status);
|
|
||||||
formatDesc = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Free parameter set buffers after submission
|
|
||||||
[parameterSetBuffers removeAllObjects];
|
|
||||||
}
|
|
||||||
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
|
|
||||||
// Construct parameter set arrays for the format description
|
|
||||||
size_t parameterSetCount = [parameterSetBuffers count];
|
|
||||||
const uint8_t* parameterSetPointers[parameterSetCount];
|
|
||||||
size_t parameterSetSizes[parameterSetCount];
|
|
||||||
for (int i = 0; i < parameterSetCount; i++) {
|
|
||||||
NSData* parameterSet = parameterSetBuffers[i];
|
|
||||||
parameterSetPointers[i] = parameterSet.bytes;
|
|
||||||
parameterSetSizes[i] = parameterSet.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log(LOG_I, @"Constructing new HEVC format description");
|
|
||||||
|
|
||||||
NSMutableDictionary* videoFormatParams = [[NSMutableDictionary alloc] init];
|
|
||||||
|
|
||||||
if (contentLightLevelInfo) {
|
|
||||||
[videoFormatParams setObject:contentLightLevelInfo forKey:(__bridge NSString*)kCMFormatDescriptionExtension_ContentLightLevelInfo];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (masteringDisplayColorVolume) {
|
|
||||||
[videoFormatParams setObject:masteringDisplayColorVolume forKey:(__bridge NSString*)kCMFormatDescriptionExtension_MasteringDisplayColorVolume];
|
|
||||||
}
|
|
||||||
|
|
||||||
status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
|
|
||||||
parameterSetCount,
|
|
||||||
parameterSetPointers,
|
|
||||||
parameterSetSizes,
|
|
||||||
NAL_LENGTH_PREFIX_SIZE,
|
|
||||||
(__bridge CFDictionaryRef)videoFormatParams,
|
|
||||||
&formatDesc);
|
|
||||||
|
|
||||||
if (status != noErr) {
|
|
||||||
Log(LOG_E, @"Failed to create HEVC format description: %d", (int)status);
|
|
||||||
formatDesc = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Free parameter set buffers after submission
|
|
||||||
[parameterSetBuffers removeAllObjects];
|
|
||||||
}
|
|
||||||
else if (videoFormat & VIDEO_FORMAT_MASK_AV1) {
|
|
||||||
NSData* fullFrameData = [NSData dataWithBytesNoCopy:data length:length freeWhenDone:NO];
|
|
||||||
|
|
||||||
Log(LOG_I, @"Constructing new AV1 format description");
|
|
||||||
formatDesc = [self createAV1FormatDescriptionForIDRFrame:fullFrameData];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Unsupported codec!
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formatDesc == NULL) {
|
|
||||||
// Can't decode if we haven't gotten our parameter sets yet
|
|
||||||
free(data);
|
|
||||||
return DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for previous decoder errors before doing anything
|
|
||||||
if (displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
|
|
||||||
Log(LOG_E, @"Display layer rendering failed: %@", displayLayer.error);
|
|
||||||
|
|
||||||
// Recreate the display layer. We are already on the main thread,
|
|
||||||
// so this is safe to do right here.
|
|
||||||
[self reinitializeDisplayLayer];
|
|
||||||
|
|
||||||
// Request an IDR frame to initialize the new decoder
|
|
||||||
free(data);
|
|
||||||
return DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we're decoding actual frame data here
|
|
||||||
CMBlockBufferRef frameBlockBuffer;
|
|
||||||
CMBlockBufferRef dataBlockBuffer;
|
|
||||||
|
|
||||||
status = CMBlockBufferCreateWithMemoryBlock(NULL, data, length, kCFAllocatorDefault, NULL, 0, length, 0, &dataBlockBuffer);
|
|
||||||
if (status != noErr) {
|
|
||||||
Log(LOG_E, @"CMBlockBufferCreateWithMemoryBlock failed: %d", (int)status);
|
|
||||||
free(data);
|
|
||||||
return DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// From now on, CMBlockBuffer owns the data pointer and will free it when it's dereferenced
|
|
||||||
|
|
||||||
status = CMBlockBufferCreateEmpty(NULL, 0, 0, &frameBlockBuffer);
|
|
||||||
if (status != noErr) {
|
|
||||||
Log(LOG_E, @"CMBlockBufferCreateEmpty failed: %d", (int)status);
|
|
||||||
CFRelease(dataBlockBuffer);
|
|
||||||
return DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// H.264 and HEVC formats require NAL prefix fixups from Annex B to length-delimited
|
|
||||||
if (videoFormat & (VIDEO_FORMAT_MASK_H264 | VIDEO_FORMAT_MASK_H265)) {
|
|
||||||
int lastOffset = -1;
|
|
||||||
for (int i = 0; i < length - NALU_START_PREFIX_SIZE; i++) {
|
|
||||||
// Search for a NALU
|
|
||||||
if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
|
|
||||||
// It's the start of a new NALU
|
|
||||||
if (lastOffset != -1) {
|
|
||||||
// We've seen a start before this so enqueue that NALU
|
|
||||||
[self updateAnnexBBufferForRange:frameBlockBuffer dataBlock:dataBlockBuffer offset:lastOffset length:i - lastOffset];
|
|
||||||
}
|
|
||||||
|
|
||||||
lastOffset = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastOffset != -1) {
|
|
||||||
// Enqueue the remaining data
|
|
||||||
[self updateAnnexBBufferForRange:frameBlockBuffer dataBlock:dataBlockBuffer offset:lastOffset length:length - lastOffset];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// For formats that require no length-changing fixups, just append a reference to the raw data block
|
|
||||||
status = CMBlockBufferAppendBufferReference(frameBlockBuffer, dataBlockBuffer, 0, length, 0);
|
|
||||||
if (status != noErr) {
|
|
||||||
Log(LOG_E, @"CMBlockBufferAppendBufferReference failed: %d", (int)status);
|
|
||||||
return DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CMSampleBufferRef sampleBuffer;
|
|
||||||
|
|
||||||
CMSampleTimingInfo sampleTiming = {kCMTimeInvalid, CMTimeMake(du->presentationTimeMs, 1000), kCMTimeInvalid};
|
|
||||||
|
|
||||||
status = CMSampleBufferCreateReady(kCFAllocatorDefault,
|
|
||||||
frameBlockBuffer,
|
|
||||||
formatDesc, 1, 1,
|
|
||||||
&sampleTiming, 0, NULL,
|
|
||||||
&sampleBuffer);
|
|
||||||
if (status != noErr) {
|
|
||||||
Log(LOG_E, @"CMSampleBufferCreate failed: %d", (int)status);
|
|
||||||
CFRelease(dataBlockBuffer);
|
|
||||||
CFRelease(frameBlockBuffer);
|
|
||||||
return DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enqueue the next frame
|
|
||||||
[self->displayLayer enqueueSampleBuffer:sampleBuffer];
|
|
||||||
|
|
||||||
if (du->frameType == FRAME_TYPE_IDR) {
|
|
||||||
// Ensure the layer is visible now
|
|
||||||
self->displayLayer.hidden = NO;
|
|
||||||
|
|
||||||
// Tell our parent VC to hide the progress indicator
|
|
||||||
[self->_callbacks videoContentShown];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dereference the buffers
|
|
||||||
CFRelease(dataBlockBuffer);
|
|
||||||
CFRelease(frameBlockBuffer);
|
|
||||||
CFRelease(sampleBuffer);
|
|
||||||
|
|
||||||
return DR_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setHdrMode:(BOOL)enabled {
|
|
||||||
SS_HDR_METADATA hdrMetadata;
|
|
||||||
|
|
||||||
BOOL hasMetadata = enabled && LiGetHdrMetadata(&hdrMetadata);
|
|
||||||
BOOL metadataChanged = NO;
|
|
||||||
|
|
||||||
if (hasMetadata && hdrMetadata.displayPrimaries[0].x != 0 && hdrMetadata.maxDisplayLuminance != 0) {
|
|
||||||
// This data is all in big-endian
|
|
||||||
struct {
|
|
||||||
vector_ushort2 primaries[3];
|
|
||||||
vector_ushort2 white_point;
|
|
||||||
uint32_t luminance_max;
|
|
||||||
uint32_t luminance_min;
|
|
||||||
} __attribute__((packed, aligned(4))) mdcv;
|
|
||||||
|
|
||||||
// mdcv is in GBR order while SS_HDR_METADATA is in RGB order
|
|
||||||
mdcv.primaries[0].x = __builtin_bswap16(hdrMetadata.displayPrimaries[1].x);
|
|
||||||
mdcv.primaries[0].y = __builtin_bswap16(hdrMetadata.displayPrimaries[1].y);
|
|
||||||
mdcv.primaries[1].x = __builtin_bswap16(hdrMetadata.displayPrimaries[2].x);
|
|
||||||
mdcv.primaries[1].y = __builtin_bswap16(hdrMetadata.displayPrimaries[2].y);
|
|
||||||
mdcv.primaries[2].x = __builtin_bswap16(hdrMetadata.displayPrimaries[0].x);
|
|
||||||
mdcv.primaries[2].y = __builtin_bswap16(hdrMetadata.displayPrimaries[0].y);
|
|
||||||
|
|
||||||
mdcv.white_point.x = __builtin_bswap16(hdrMetadata.whitePoint.x);
|
|
||||||
mdcv.white_point.y = __builtin_bswap16(hdrMetadata.whitePoint.y);
|
|
||||||
|
|
||||||
// These luminance values are in 10000ths of a nit
|
|
||||||
mdcv.luminance_max = __builtin_bswap32((uint32_t)hdrMetadata.maxDisplayLuminance * 10000);
|
|
||||||
mdcv.luminance_min = __builtin_bswap32(hdrMetadata.minDisplayLuminance);
|
|
||||||
|
|
||||||
NSData* newMdcv = [NSData dataWithBytes:&mdcv length:sizeof(mdcv)];
|
|
||||||
if (masteringDisplayColorVolume == nil || ![newMdcv isEqualToData:masteringDisplayColorVolume]) {
|
|
||||||
masteringDisplayColorVolume = newMdcv;
|
|
||||||
metadataChanged = YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (masteringDisplayColorVolume != nil) {
|
|
||||||
masteringDisplayColorVolume = nil;
|
|
||||||
metadataChanged = YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasMetadata && hdrMetadata.maxContentLightLevel != 0 && hdrMetadata.maxFrameAverageLightLevel != 0) {
|
|
||||||
// This data is all in big-endian
|
|
||||||
struct {
|
|
||||||
uint16_t max_content_light_level;
|
|
||||||
uint16_t max_frame_average_light_level;
|
|
||||||
} __attribute__((packed, aligned(2))) cll;
|
|
||||||
|
|
||||||
cll.max_content_light_level = __builtin_bswap16(hdrMetadata.maxContentLightLevel);
|
|
||||||
cll.max_frame_average_light_level = __builtin_bswap16(hdrMetadata.maxFrameAverageLightLevel);
|
|
||||||
|
|
||||||
NSData* newCll = [NSData dataWithBytes:&cll length:sizeof(cll)];
|
|
||||||
if (contentLightLevelInfo == nil || ![newCll isEqualToData:contentLightLevelInfo]) {
|
|
||||||
contentLightLevelInfo = newCll;
|
|
||||||
metadataChanged = YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (contentLightLevelInfo != nil) {
|
|
||||||
contentLightLevelInfo = nil;
|
|
||||||
metadataChanged = YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the metadata changed, request an IDR frame to re-create the CMVideoFormatDescription
|
|
||||||
if (metadataChanged) {
|
|
||||||
LiRequestIdrFrame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
//
|
|
||||||
// UIAppView.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 10/22/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
#import "TemporaryApp.h"
|
|
||||||
|
|
||||||
@protocol AppCallback <NSObject>
|
|
||||||
|
|
||||||
- (void) appClicked:(TemporaryApp*)app view:(UIView*)view;
|
|
||||||
- (void) appLongClicked:(TemporaryApp*)app view:(UIView*)view;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
@interface UIAppView : UIButton <UIContextMenuInteractionDelegate>
|
|
||||||
#else
|
|
||||||
@interface UIAppView : UIButton
|
|
||||||
#endif
|
|
||||||
|
|
||||||
- (id) initWithApp:(TemporaryApp*)app cache:(NSCache*)cache andCallback:(id<AppCallback>)callback;
|
|
||||||
- (void) updateAppImage;
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
//
|
|
||||||
// UIAppView.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 10/22/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "UIAppView.h"
|
|
||||||
#import "AppAssetManager.h"
|
|
||||||
|
|
||||||
static const float REFRESH_CYCLE = 1.0f;
|
|
||||||
|
|
||||||
@implementation UIAppView {
|
|
||||||
TemporaryApp* _app;
|
|
||||||
UILabel* _appLabel;
|
|
||||||
UIImageView* _appOverlay;
|
|
||||||
UIImageView* _appImage;
|
|
||||||
NSCache* _artCache;
|
|
||||||
id<AppCallback> _callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
static UIImage* noImage;
|
|
||||||
|
|
||||||
- (id) initWithApp:(TemporaryApp*)app cache:(NSCache*)cache andCallback:(id<AppCallback>)callback {
|
|
||||||
self = [super init];
|
|
||||||
_app = app;
|
|
||||||
_callback = callback;
|
|
||||||
_artCache = cache;
|
|
||||||
|
|
||||||
// Cache the NoAppImage ourselves to avoid
|
|
||||||
// having to load it each time
|
|
||||||
if (noImage == nil) {
|
|
||||||
noImage = [UIImage imageNamed:@"NoAppImage"];
|
|
||||||
}
|
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
|
||||||
self.frame = CGRectMake(0, 0, 200, 265);
|
|
||||||
#else
|
|
||||||
self.frame = CGRectMake(0, 0, 150, 200);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
[self setAlpha:app.hidden ? 0.4 : 1.0];
|
|
||||||
|
|
||||||
_appImage = [[UIImageView alloc] initWithFrame:self.frame];
|
|
||||||
[_appImage setImage:noImage];
|
|
||||||
[self addSubview:_appImage];
|
|
||||||
|
|
||||||
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
|
|
||||||
// for tvOS devices and iOS prior to 13.0.
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
if (@available(iOS 13.0, *)) {
|
|
||||||
UIContextMenuInteraction* rightClickInteraction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
|
|
||||||
[self addInteraction:rightClickInteraction];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(appLongClicked:)];
|
|
||||||
[self addGestureRecognizer:longPressRecognizer];
|
|
||||||
}
|
|
||||||
|
|
||||||
[self addTarget:self action:@selector(appClicked:) forControlEvents:UIControlEventPrimaryActionTriggered];
|
|
||||||
|
|
||||||
[self addTarget:self action:@selector(buttonSelected:) forControlEvents:UIControlEventTouchDown];
|
|
||||||
[self addTarget:self action:@selector(buttonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
|
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
|
||||||
_appImage.adjustsImageWhenAncestorFocused = YES;
|
|
||||||
#else
|
|
||||||
// Rasterizing the cell layer increases rendering performance by quite a bit
|
|
||||||
// but we want it unrasterized for tvOS where it must be scaled.
|
|
||||||
self.layer.shouldRasterize = YES;
|
|
||||||
self.layer.rasterizationScale = [UIScreen mainScreen].scale;
|
|
||||||
|
|
||||||
if (@available(iOS 13.4.1, *)) {
|
|
||||||
// Allow the button style to change when moused over
|
|
||||||
self.pointerInteractionEnabled = YES;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
[self updateAppImage];
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)didMoveToSuperview {
|
|
||||||
// Start our update loop when we are added to our cell
|
|
||||||
if (self.superview != nil) {
|
|
||||||
[self updateLoop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) appClicked:(UIView *)view {
|
|
||||||
[_callback appClicked:_app view:view];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) appLongClicked:(UILongPressGestureRecognizer*)gesture {
|
|
||||||
if (gesture.state == UIGestureRecognizerStateBegan) {
|
|
||||||
[_callback appLongClicked:_app view:self];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
- (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction
|
|
||||||
configurationForMenuAtLocation:(CGPoint)location {
|
|
||||||
// We don't want to trigger the primary action at this point, so cancel
|
|
||||||
// tracking touch on this view now. This will also have the (intended)
|
|
||||||
// effect of removing the touch highlight on this view.
|
|
||||||
[self cancelTrackingWithEvent:nil];
|
|
||||||
|
|
||||||
[_callback appLongClicked:_app view:self];
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
- (void) updateAppImage {
|
|
||||||
if (_appOverlay != nil) {
|
|
||||||
[_appOverlay removeFromSuperview];
|
|
||||||
_appOverlay = nil;
|
|
||||||
}
|
|
||||||
if (_appLabel != nil) {
|
|
||||||
[_appLabel removeFromSuperview];
|
|
||||||
_appLabel = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
BOOL noAppImage = false;
|
|
||||||
|
|
||||||
// First check the memory cache
|
|
||||||
UIImage* appImage = [_artCache objectForKey:_app];
|
|
||||||
if (appImage == nil) {
|
|
||||||
// Next try to load from the on disk cache
|
|
||||||
appImage = [UIImage imageWithContentsOfFile:[AppAssetManager boxArtPathForApp:_app]];
|
|
||||||
if (appImage != nil) {
|
|
||||||
[_artCache setObject:appImage forKey:_app];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appImage != nil) {
|
|
||||||
// This size of image might be blank image received from GameStream.
|
|
||||||
// TODO: Improve no-app image detection
|
|
||||||
if (!(appImage.size.width == 130.f && appImage.size.height == 180.f) && // GFE 2.0
|
|
||||||
!(appImage.size.width == 628.f && appImage.size.height == 888.f)) { // GFE 3.0
|
|
||||||
[_appImage setImage:appImage];
|
|
||||||
} else {
|
|
||||||
noAppImage = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
noAppImage = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([_app.id isEqualToString:_app.host.currentGame]) {
|
|
||||||
// Only create the app overlay if needed
|
|
||||||
_appOverlay = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Play"]];
|
|
||||||
_appOverlay.layer.shadowColor = [UIColor blackColor].CGColor;
|
|
||||||
_appOverlay.layer.shadowOffset = CGSizeMake(0, 0);
|
|
||||||
_appOverlay.layer.shadowOpacity = 1;
|
|
||||||
_appOverlay.layer.shadowRadius = 4.0;
|
|
||||||
_appOverlay.contentMode = UIViewContentModeScaleAspectFit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noAppImage) {
|
|
||||||
_appLabel = [[UILabel alloc] init];
|
|
||||||
[_appLabel setTextColor:[UIColor whiteColor]];
|
|
||||||
[_appLabel setText:_app.name];
|
|
||||||
[_appLabel setFont:[UIFont systemFontOfSize:24]];
|
|
||||||
[_appLabel setBaselineAdjustment:UIBaselineAdjustmentAlignCenters];
|
|
||||||
[_appLabel setTextAlignment:NSTextAlignmentCenter];
|
|
||||||
[_appLabel setLineBreakMode:NSLineBreakByWordWrapping];
|
|
||||||
[_appLabel setNumberOfLines:0];
|
|
||||||
}
|
|
||||||
|
|
||||||
[self positionSubviews];
|
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
|
||||||
[_appImage.overlayContentView addSubview:_appLabel];
|
|
||||||
[_appImage.overlayContentView addSubview:_appOverlay];
|
|
||||||
#else
|
|
||||||
[self addSubview:_appLabel];
|
|
||||||
[self addSubview:_appOverlay];
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) buttonSelected:(id)sender {
|
|
||||||
_appImage.layer.opacity = 0.5f;
|
|
||||||
}
|
|
||||||
- (void) buttonDeselected:(id)sender {
|
|
||||||
_appImage.layer.opacity = 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) positionSubviews {
|
|
||||||
CGFloat padding = 5.f;
|
|
||||||
CGSize frameSize = _appImage.frame.size;
|
|
||||||
CGPoint center = _appImage.center;
|
|
||||||
|
|
||||||
if (_appLabel != nil) {
|
|
||||||
if (_appOverlay != nil) {
|
|
||||||
_appOverlay.frame = CGRectMake(0, 0, frameSize.width / 3, frameSize.width / 3);
|
|
||||||
_appOverlay.center = CGPointMake(frameSize.width / 2, padding + _appOverlay.frame.size.height / 2);
|
|
||||||
|
|
||||||
[_appLabel setFrame:CGRectMake(padding, _appOverlay.frame.size.height + padding, frameSize.width - 2 * padding, frameSize.height - _appOverlay.frame.size.height - 2 * padding)];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[_appLabel setFrame:CGRectMake(padding, padding, frameSize.width - 2 * padding, frameSize.height - 2 * padding)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (_appOverlay != nil) {
|
|
||||||
_appOverlay.frame = CGRectMake(0, 0, frameSize.width / 2, frameSize.width / 2);
|
|
||||||
_appOverlay.center = center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) updateLoop {
|
|
||||||
// Stop immediately if the view has been detached
|
|
||||||
if (self.superview == nil) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the app image if neccessary
|
|
||||||
if ((_appOverlay != nil && ![_app.id isEqualToString:_app.host.currentGame]) ||
|
|
||||||
(_appOverlay == nil && [_app.id isEqualToString:_app.host.currentGame])) {
|
|
||||||
[self updateAppImage];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show no shadow for hidden apps. Because we adjust the opacity of the
|
|
||||||
// cells for hidden apps, it makes them look bad when the shadow draws
|
|
||||||
// through the app tile.
|
|
||||||
self.superview.layer.shadowOpacity = _app.hidden ? 0.0f : 0.5f;
|
|
||||||
|
|
||||||
// Update opacity if neccessary
|
|
||||||
[self setAlpha:_app.hidden ? 0.4 : 1.0];
|
|
||||||
|
|
||||||
// Queue the next refresh cycle
|
|
||||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
//
|
|
||||||
// UIComputerView.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 10/22/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
#import "TemporaryHost.h"
|
|
||||||
|
|
||||||
@protocol HostCallback <NSObject>
|
|
||||||
|
|
||||||
- (void) hostClicked:(TemporaryHost*)host view:(UIView*)view;
|
|
||||||
- (void) hostLongClicked:(TemporaryHost*)host view:(UIView*)view;
|
|
||||||
- (void) addHostClicked;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
@interface UIComputerView : UIButton <UIContextMenuInteractionDelegate>
|
|
||||||
#else
|
|
||||||
@interface UIComputerView : UIButton
|
|
||||||
#endif
|
|
||||||
|
|
||||||
- (id) initWithComputer:(TemporaryHost*)host andCallback:(id<HostCallback>)callback;
|
|
||||||
- (id) initForAddWithCallback:(id<HostCallback>)callback;
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
//
|
|
||||||
// UIComputerView.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 10/22/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "UIComputerView.h"
|
|
||||||
#import "LocalizationHelper.h"
|
|
||||||
|
|
||||||
@implementation UIComputerView {
|
|
||||||
TemporaryHost* _host;
|
|
||||||
UIImageView* _hostIcon;
|
|
||||||
UILabel* _hostLabel;
|
|
||||||
UIImageView* _hostOverlay;
|
|
||||||
UIActivityIndicatorView* _hostSpinner;
|
|
||||||
id<HostCallback> _callback;
|
|
||||||
CGSize _labelSize;
|
|
||||||
}
|
|
||||||
static const float REFRESH_CYCLE = 2.0f;
|
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
|
||||||
static const int ITEM_PADDING = 50;
|
|
||||||
static const int LABEL_DY = 40;
|
|
||||||
#else
|
|
||||||
static const int ITEM_PADDING = 0;
|
|
||||||
static const int LABEL_DY = 20;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
- (id) init {
|
|
||||||
self = [super init];
|
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
|
||||||
self.frame = CGRectMake(0, 0, 400, 400);
|
|
||||||
#else
|
|
||||||
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
|
|
||||||
self.frame = CGRectMake(0, 0, 200, 200);
|
|
||||||
} else {
|
|
||||||
self.frame = CGRectMake(0, 0, 100, 100);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
_hostIcon = [[UIImageView alloc] initWithFrame:self.frame];
|
|
||||||
[_hostIcon setImage:[UIImage imageNamed:@"Computer"]];
|
|
||||||
|
|
||||||
self.layer.shadowColor = [[UIColor blackColor] CGColor];
|
|
||||||
self.layer.shadowOffset = CGSizeMake(5,8);
|
|
||||||
self.layer.shadowOpacity = 0.3;
|
|
||||||
|
|
||||||
[self addTarget:self action:@selector(hostButtonSelected:) forControlEvents:UIControlEventTouchDown];
|
|
||||||
[self addTarget:self action:@selector(hostButtonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
|
|
||||||
|
|
||||||
_hostLabel = [[UILabel alloc] init];
|
|
||||||
_hostLabel.textColor = [UIColor whiteColor];
|
|
||||||
|
|
||||||
_hostOverlay = [[UIImageView alloc] initWithFrame:CGRectMake(self.frame.size.width / 3, _hostIcon.frame.size.height / 4, _hostIcon.frame.size.width / 3, self.frame.size.height / 3)];
|
|
||||||
_hostSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
|
|
||||||
[_hostSpinner setFrame:_hostOverlay.frame];
|
|
||||||
_hostSpinner.userInteractionEnabled = NO;
|
|
||||||
_hostSpinner.hidesWhenStopped = YES;
|
|
||||||
|
|
||||||
[self addSubview:_hostLabel];
|
|
||||||
[self addSubview:_hostIcon];
|
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
|
||||||
_hostIcon.clipsToBounds = NO;
|
|
||||||
_hostIcon.adjustsImageWhenAncestorFocused = YES;
|
|
||||||
_hostIcon.masksFocusEffectToContents = YES;
|
|
||||||
|
|
||||||
self.adjustsImageWhenHighlighted = NO;
|
|
||||||
|
|
||||||
_hostOverlay.masksFocusEffectToContents = YES;
|
|
||||||
_hostOverlay.adjustsImageWhenAncestorFocused = NO;
|
|
||||||
|
|
||||||
[_hostIcon.overlayContentView addSubview:_hostOverlay];
|
|
||||||
[_hostIcon.overlayContentView addSubview:_hostSpinner];
|
|
||||||
#else
|
|
||||||
[self addSubview:_hostOverlay];
|
|
||||||
[self addSubview:_hostSpinner];
|
|
||||||
|
|
||||||
if (@available(iOS 13.4.1, *)) {
|
|
||||||
// Allow the button style to change when moused over
|
|
||||||
self.pointerInteractionEnabled = YES;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) hostButtonSelected:(id)sender {
|
|
||||||
_hostIcon.layer.opacity = 0.5f;
|
|
||||||
_hostSpinner.layer.opacity = 0.5f;
|
|
||||||
_hostOverlay.layer.opacity = 0.5f;
|
|
||||||
}
|
|
||||||
- (void) hostButtonDeselected:(id)sender {
|
|
||||||
_hostIcon.layer.opacity = 1.0f;
|
|
||||||
_hostSpinner.layer.opacity = 1.0f;
|
|
||||||
_hostOverlay.layer.opacity = 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id) initForAddWithCallback:(id<HostCallback>)callback {
|
|
||||||
self = [self init];
|
|
||||||
_callback = callback;
|
|
||||||
|
|
||||||
[self addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
|
||||||
|
|
||||||
[_hostLabel setText:[LocalizationHelper localizedStringForKey:@"Add Host Manually"]];
|
|
||||||
[_hostLabel sizeToFit];
|
|
||||||
|
|
||||||
[_hostOverlay setImage:[UIImage imageNamed:@"AddOverlayIcon"]];
|
|
||||||
|
|
||||||
[self updateBounds];
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id) initWithComputer:(TemporaryHost*)host andCallback:(id<HostCallback>)callback {
|
|
||||||
self = [self init];
|
|
||||||
_host = host;
|
|
||||||
_callback = callback;
|
|
||||||
|
|
||||||
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
|
|
||||||
// for tvOS devices and iOS prior to 13.0.
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
if (@available(iOS 13.0, *)) {
|
|
||||||
UIContextMenuInteraction* rightClickInteraction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
|
|
||||||
[self addInteraction:rightClickInteraction];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked:)];
|
|
||||||
[self addGestureRecognizer:longPressRecognizer];
|
|
||||||
}
|
|
||||||
|
|
||||||
[self addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
|
||||||
|
|
||||||
[self updateContentsForHost:host];
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)didMoveToSuperview {
|
|
||||||
// Start our update loop when we are added to our cell
|
|
||||||
if (self.superview != nil && _host != nil) {
|
|
||||||
[self updateLoop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) updateBounds {
|
|
||||||
float x = FLT_MAX;
|
|
||||||
float y = FLT_MAX;
|
|
||||||
float width = 0;
|
|
||||||
float height;
|
|
||||||
|
|
||||||
float iconX = _hostIcon.frame.origin.x + _hostIcon.frame.size.width / 2;
|
|
||||||
_hostLabel.center = CGPointMake(iconX, _hostIcon.frame.origin.y + _hostIcon.frame.size.height + LABEL_DY);
|
|
||||||
|
|
||||||
x = MIN(x, _hostIcon.frame.origin.x);
|
|
||||||
x = MIN(x, _hostLabel.frame.origin.x);
|
|
||||||
|
|
||||||
y = MIN(y, _hostIcon.frame.origin.y);
|
|
||||||
y = MIN(y, _hostLabel.frame.origin.y);
|
|
||||||
|
|
||||||
width = MAX(width, _hostIcon.frame.size.width);
|
|
||||||
width = MAX(width, _hostLabel.frame.size.width);
|
|
||||||
|
|
||||||
height = _hostIcon.frame.size.height +
|
|
||||||
_hostLabel.frame.size.height +
|
|
||||||
LABEL_DY / 2;
|
|
||||||
|
|
||||||
self.bounds = CGRectMake(x - ITEM_PADDING, y - ITEM_PADDING, width + 2 * ITEM_PADDING, height + 2 * ITEM_PADDING);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) updateContentsForHost:(TemporaryHost*)host {
|
|
||||||
_hostLabel.text = _host.name;
|
|
||||||
[_hostLabel sizeToFit];
|
|
||||||
|
|
||||||
if (host.state == StateOnline) {
|
|
||||||
[_hostSpinner stopAnimating];
|
|
||||||
|
|
||||||
if (host.pairState == PairStateUnpaired) {
|
|
||||||
[_hostOverlay setImage:[UIImage imageNamed:@"LockedOverlayIcon"]];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[_hostOverlay setImage:nil];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (host.state == StateOffline) {
|
|
||||||
[_hostSpinner stopAnimating];
|
|
||||||
[_hostOverlay setImage:[UIImage imageNamed:@"ErrorOverlayIcon"]];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[_hostSpinner startAnimating];
|
|
||||||
}
|
|
||||||
|
|
||||||
[self updateBounds];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) updateLoop {
|
|
||||||
// Stop immediately if the view has been detached
|
|
||||||
if (self.superview == nil) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
[self updateContentsForHost:_host];
|
|
||||||
|
|
||||||
// Queue the next refresh cycle
|
|
||||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) hostLongClicked:(UILongPressGestureRecognizer*)gesture {
|
|
||||||
if (gesture.state == UIGestureRecognizerStateBegan) {
|
|
||||||
[_callback hostLongClicked:_host view:self];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
- (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction
|
|
||||||
configurationForMenuAtLocation:(CGPoint)location {
|
|
||||||
// We don't want to trigger the primary action at this point, so cancel
|
|
||||||
// tracking touch on this view now. This will also have the (intended)
|
|
||||||
// effect of removing the touch highlight on this view.
|
|
||||||
[self cancelTrackingWithEvent:nil];
|
|
||||||
|
|
||||||
[_callback hostLongClicked:_host view:self];
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
- (void) hostClicked {
|
|
||||||
[_callback hostClicked:_host view:self];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) addClicked {
|
|
||||||
[_callback addHostClicked];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
//
|
|
||||||
// Logger.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 2/10/15.
|
|
||||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef Limelight_Logger_h
|
|
||||||
#define Limelight_Logger_h
|
|
||||||
|
|
||||||
#import <stdarg.h>
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
LOG_D,
|
|
||||||
LOG_I,
|
|
||||||
LOG_W,
|
|
||||||
LOG_E
|
|
||||||
} LogLevel;
|
|
||||||
|
|
||||||
#define PRFX_DEBUG @"<DEBUG>"
|
|
||||||
#define PRFX_INFO @"<INFO>"
|
|
||||||
#define PRFX_WARN @"<WARN>"
|
|
||||||
#define PRFX_ERROR @"<ERROR>"
|
|
||||||
|
|
||||||
void Log(LogLevel level, NSString* fmt, ...);
|
|
||||||
void LogTag(LogLevel level, NSString* tag, NSString* fmt, ...);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
//
|
|
||||||
// KeyManagerViewController.swift
|
|
||||||
// Moonlight-ZWM
|
|
||||||
//
|
|
||||||
// Created by ZWM on 2024/7/23.
|
|
||||||
// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
@objc public class CommandManagerViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
|
|
||||||
|
|
||||||
public let tableView = UITableView()
|
|
||||||
private let addButton = UIButton(type: .system)
|
|
||||||
private let deleteButton = UIButton(type: .system)
|
|
||||||
private let editButton = UIButton(type: .system)
|
|
||||||
private let exitButton = UIButton(type: .system)
|
|
||||||
private let pinButton = UIButton(type: .system)
|
|
||||||
private let viewBackgroundColor = UIColor(white: 0.2, alpha: 0.8);
|
|
||||||
private let highlightColor = UIColor(white: 0.39, alpha: 0.8);
|
|
||||||
private let titleLabel = UILabel()
|
|
||||||
|
|
||||||
private var viewPinned: Bool = false
|
|
||||||
private var isEditingMode: Bool = false {
|
|
||||||
didSet {
|
|
||||||
updateEditingMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
setupViews()
|
|
||||||
updateEditingMode()
|
|
||||||
|
|
||||||
tableView.dataSource = self
|
|
||||||
tableView.delegate = self
|
|
||||||
|
|
||||||
// Register the cell class
|
|
||||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
|
|
||||||
//pass self to the CommandManager
|
|
||||||
CommandManager.shared.viewController = self
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func viewDidLayoutSubviews() {
|
|
||||||
super.viewDidLayoutSubviews()
|
|
||||||
setupConstraints()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupViews() {
|
|
||||||
|
|
||||||
// Set corner radius
|
|
||||||
view.layer.cornerRadius = 20 // Adjust the corner radius
|
|
||||||
view.layer.masksToBounds = true
|
|
||||||
|
|
||||||
// Set up the title label
|
|
||||||
titleLabel.text = SwiftLocalizationHelper.localizedString(forKey: "Send Commands")
|
|
||||||
titleLabel.font = UIFont.boldSystemFont(ofSize: 20) // Adjust font size as needed
|
|
||||||
titleLabel.textColor = UIColor.white // Adjust color as needed
|
|
||||||
titleLabel.textAlignment = .center
|
|
||||||
view.addSubview(titleLabel)
|
|
||||||
|
|
||||||
view.backgroundColor = viewBackgroundColor
|
|
||||||
tableView.backgroundColor = .clear
|
|
||||||
tableView.rowHeight = 60
|
|
||||||
tableView.separatorColor = highlightColor
|
|
||||||
// Configure buttons
|
|
||||||
addButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Add / Duplicate"), for: .normal)
|
|
||||||
deleteButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Delete"), for: .normal)
|
|
||||||
editButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Edit"), for: .normal)
|
|
||||||
exitButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Exit"), for: .normal)
|
|
||||||
pinButton.setTitle("📌", for: .normal)
|
|
||||||
addButton.titleLabel?.font = UIFont.systemFont(ofSize: 20) // Adjust the size as needed
|
|
||||||
deleteButton.titleLabel?.font = UIFont.systemFont(ofSize: 20)
|
|
||||||
editButton.titleLabel?.font = UIFont.systemFont(ofSize: 20)
|
|
||||||
exitButton.titleLabel?.font = UIFont.systemFont(ofSize: 20)
|
|
||||||
pinButton.titleLabel?.font = UIFont.systemFont(ofSize: 20)
|
|
||||||
|
|
||||||
// Add subviews
|
|
||||||
view.addSubview(tableView)
|
|
||||||
view.addSubview(addButton)
|
|
||||||
view.addSubview(deleteButton)
|
|
||||||
view.addSubview(editButton)
|
|
||||||
view.addSubview(exitButton)
|
|
||||||
view.addSubview(pinButton)
|
|
||||||
|
|
||||||
// Setup button targets
|
|
||||||
addButton.addTarget(self, action: #selector(addButtonTapped), for: .touchUpInside)
|
|
||||||
deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside)
|
|
||||||
editButton.addTarget(self, action: #selector(editButtonTapped), for: .touchUpInside)
|
|
||||||
exitButton.addTarget(self, action: #selector(exitButtonTapped), for: .touchUpInside)
|
|
||||||
pinButton.addTarget(self, action: #selector(pinButtonTapped), for: .touchUpInside)
|
|
||||||
pinButton.contentEdgeInsets = UIEdgeInsets(top: 7, left: 20, bottom: 7, right: 20)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupConstraints() {
|
|
||||||
|
|
||||||
guard let superview = view.superview else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addButton.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
deleteButton.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
editButton.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
exitButton.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
pinButton.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
|
|
||||||
// Set the width and height of the view
|
|
||||||
view.widthAnchor.constraint(equalTo: view.superview!.widthAnchor, multiplier: 1),
|
|
||||||
view.heightAnchor.constraint(equalTo: view.superview!.heightAnchor, multiplier: 1),
|
|
||||||
|
|
||||||
// Center the view horizontally and vertically
|
|
||||||
view.centerXAnchor.constraint(equalTo: view.superview!.centerXAnchor),
|
|
||||||
view.centerYAnchor.constraint(equalTo: view.superview!.centerYAnchor),
|
|
||||||
|
|
||||||
// ViewTitle constrains
|
|
||||||
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 13.5),
|
|
||||||
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
|
|
||||||
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
|
|
||||||
|
|
||||||
|
|
||||||
// TableView constraints
|
|
||||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
|
|
||||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
|
|
||||||
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 50),
|
|
||||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -60),
|
|
||||||
|
|
||||||
// ExitButton constraints
|
|
||||||
exitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25),
|
|
||||||
exitButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
|
|
||||||
|
|
||||||
// EditButton constraints
|
|
||||||
editButton.leadingAnchor.constraint(equalTo: exitButton.trailingAnchor, constant: 50),
|
|
||||||
editButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
|
|
||||||
|
|
||||||
// AddButton constraints
|
|
||||||
addButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -25),
|
|
||||||
addButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
|
|
||||||
|
|
||||||
// DeleteButton constraints
|
|
||||||
deleteButton.trailingAnchor.constraint(equalTo: addButton.leadingAnchor, constant: -50),
|
|
||||||
deleteButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
|
|
||||||
|
|
||||||
// DeleteButton constraints
|
|
||||||
pinButton.trailingAnchor.constraint(equalTo: deleteButton.leadingAnchor, constant: -50),
|
|
||||||
pinButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private func updateEditingMode() {
|
|
||||||
addButton.isEnabled = isEditingMode
|
|
||||||
deleteButton.isEnabled = isEditingMode
|
|
||||||
if(isEditingMode){ editButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Done"), for: .normal) }
|
|
||||||
else{ editButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Edit"), for: .normal) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func pinButtonTapped() {
|
|
||||||
viewPinned = !viewPinned
|
|
||||||
if viewPinned {
|
|
||||||
pinButton.backgroundColor = highlightColor
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
pinButton.backgroundColor = viewBackgroundColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func addButtonTapped() {
|
|
||||||
let previouslySelectedIndexPath = tableView.indexPathForSelectedRow //memorize selected indexpath
|
|
||||||
let alert = UIAlertController(title: SwiftLocalizationHelper.localizedString(forKey: "New Command"), message: SwiftLocalizationHelper.localizedString(forKey: "Enter a new command and alias"), preferredStyle: .alert)
|
|
||||||
alert.addTextField { $0.placeholder = SwiftLocalizationHelper.localizedString(forKey:"Command") }
|
|
||||||
alert.addTextField { $0.placeholder = SwiftLocalizationHelper.localizedString(forKey: "Alias (optional)") }
|
|
||||||
alert.textFields?[0].keyboardType = .asciiCapable
|
|
||||||
alert.textFields?[0].autocorrectionType = .no
|
|
||||||
alert.textFields?[0].spellCheckingType = .no
|
|
||||||
alert.textFields?[1].keyboardType = .asciiCapable
|
|
||||||
alert.textFields?[1].autocorrectionType = .no
|
|
||||||
alert.textFields?[1].spellCheckingType = .no
|
|
||||||
|
|
||||||
let submitAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Add"), style: .default) { [unowned alert] _ in
|
|
||||||
let keyboardCmdString = alert.textFields?[0].text ?? ""
|
|
||||||
let alias = alert.textFields?[1].text ?? keyboardCmdString
|
|
||||||
let newCommand = RemoteCommand(keyboardCmdString: keyboardCmdString, alias: alias)
|
|
||||||
let addCommandSuceeded = CommandManager.shared.addCommand(newCommand)
|
|
||||||
//self.reloadTableView() // don't know why but this reload has to be called from the CommandManager, it doesn't work here.
|
|
||||||
|
|
||||||
//if previouslySelectedIndexPath == nil { return }
|
|
||||||
if addCommandSuceeded {
|
|
||||||
let lastRow = self.tableView.numberOfRows(inSection: 0) - 1 //for now there's only 1 section for the tableview, just use setion 0
|
|
||||||
let newEntryIndexPath = IndexPath(row: lastRow, section: 0)
|
|
||||||
self.tableView.selectRow(at: newEntryIndexPath, animated: true, scrollPosition: .middle) // shift the highlight to the newly added entry
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.tableView.selectRow(at: previouslySelectedIndexPath, animated: true, scrollPosition: .middle) // keep the highlight on the previous entry if failed to add command
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let cancelAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey:"Cancel"), style: .cancel)
|
|
||||||
alert.addAction(submitAction)
|
|
||||||
alert.addAction(cancelAction)
|
|
||||||
|
|
||||||
if let selectedIndexPath = self.tableView.indexPathForSelectedRow {
|
|
||||||
let selectedCommand = CommandManager.shared.getAllCommands()[selectedIndexPath.row]
|
|
||||||
alert.textFields?[0].text = selectedCommand.keyboardCmdString // load selected keyboard cmd string
|
|
||||||
//alert.textFields?[1].text = selectedCommand.alias // leave the alias input field empty
|
|
||||||
}
|
|
||||||
|
|
||||||
self.present(alert, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@objc private func deleteButtonTapped() {
|
|
||||||
guard let selectedIndexPath = tableView.indexPathForSelectedRow else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let previouslySelectedIndexPath = selectedIndexPath // memorize selected row before reloading tableview
|
|
||||||
|
|
||||||
CommandManager.shared.deleteCommand(at: selectedIndexPath.row)
|
|
||||||
reloadTableView()
|
|
||||||
// target new entry after deletion
|
|
||||||
var targetRowAfterDel = previouslySelectedIndexPath.row - 1
|
|
||||||
if targetRowAfterDel < 0 { targetRowAfterDel = 0 }
|
|
||||||
let targetIndexPathAfterDel = IndexPath(row: targetRowAfterDel, section: previouslySelectedIndexPath.section)
|
|
||||||
if targetRowAfterDel < tableView.numberOfRows(inSection: previouslySelectedIndexPath.section) {
|
|
||||||
tableView.selectRow(at: targetIndexPathAfterDel, animated: true, scrollPosition: .middle) // shift highlight to the target entry after deletion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func editButtonTapped() {
|
|
||||||
isEditingMode.toggle()
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func exitButtonTapped() {
|
|
||||||
dismiss(animated: true, completion: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func reloadTableView() {
|
|
||||||
tableView.layoutIfNeeded()
|
|
||||||
tableView.reloadData()
|
|
||||||
|
|
||||||
/*
|
|
||||||
let previouslySelectedIndexPath = tableView.indexPathForSelectedRow
|
|
||||||
if let indexPath = previouslySelectedIndexPath {
|
|
||||||
// Make sure the indexPath is still valid and scroll to the selected indexPath
|
|
||||||
if indexPath.row < tableView.numberOfRows(inSection: indexPath.section) {
|
|
||||||
tableView.selectRow(at: indexPath, animated: true, scrollPosition: .middle) // keep the entry of previous index selected.
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
|
|
||||||
// UITableViewDataSource
|
|
||||||
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
||||||
return CommandManager.shared.getAllCommands().count
|
|
||||||
}
|
|
||||||
|
|
||||||
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
|
|
||||||
// Configure cell appearance
|
|
||||||
cell.backgroundColor = .clear // Set background color of the cell
|
|
||||||
cell.textLabel?.textColor = UIColor.white // Set font color of the text
|
|
||||||
|
|
||||||
|
|
||||||
// Set selected background view
|
|
||||||
let selectedBackgroundView = UIView()
|
|
||||||
selectedBackgroundView.backgroundColor = highlightColor // Color for the selected state
|
|
||||||
cell.selectedBackgroundView = selectedBackgroundView
|
|
||||||
|
|
||||||
let command = CommandManager.shared.getAllCommands()[indexPath.row]
|
|
||||||
cell.textLabel?.text = command.alias
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
|
|
||||||
// UITableViewDelegate
|
|
||||||
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
||||||
|
|
||||||
if let cell = tableView.cellForRow(at: indexPath) {
|
|
||||||
// Set the cell background color to the flashing color
|
|
||||||
// Animate the flash effect
|
|
||||||
UIView.animate(withDuration: 0.1, animations: {
|
|
||||||
cell.selectedBackgroundView?.backgroundColor = .clear
|
|
||||||
}) { _ in
|
|
||||||
// Reset the cell background color after the animation
|
|
||||||
UIView.animate(withDuration: 0.1) {
|
|
||||||
cell.selectedBackgroundView?.backgroundColor = self.highlightColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isEditingMode {
|
|
||||||
// Sending keyboard command
|
|
||||||
let command = CommandManager.shared.getAllCommands()[indexPath.row]
|
|
||||||
sendKeyboardCommand(command)
|
|
||||||
if !viewPinned { dismiss(animated: false, completion: nil) } // dimiss the view in sending mode & the view is not pinned
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func sendKeyboardCommand(_ cmd: RemoteCommand) {
|
|
||||||
print("Sending key-value")
|
|
||||||
let keyboardCmdStrings = CommandManager.shared.extractKeyStringsFromComboCommand(from: cmd.keyboardCmdString)
|
|
||||||
CommandManager.shared.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
//
|
|
||||||
// ProfileTableViewCell.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Long Le on 12/11/22.
|
|
||||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
@interface ProfileTableViewCell : UITableViewCell
|
|
||||||
|
|
||||||
@property (weak, nonatomic) IBOutlet UILabel *name;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
//
|
|
||||||
// ProfileTableViewCell.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Long Le on 12/11/22.
|
|
||||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "ProfileTableViewCell.h"
|
|
||||||
|
|
||||||
@implementation ProfileTableViewCell
|
|
||||||
|
|
||||||
- (void)awakeFromNib {
|
|
||||||
[super awakeFromNib];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
|
|
||||||
[super setSelected:selected animated:animated];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
|
||||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
|
||||||
<dependencies>
|
|
||||||
<deployment identifier="iOS"/>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
||||||
</dependencies>
|
|
||||||
<objects>
|
|
||||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
|
||||||
<tableViewCell contentMode="scaleToFill" selectionStyle="none" accessoryType="checkmark" indentationWidth="10" reuseIdentifier="Cell" id="KGk-i7-Jjw" customClass="ProfileTableViewCell">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="280" height="44"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<subviews>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Profile Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="b1d-b4-TCK">
|
|
||||||
<rect key="frame" x="16.000000000000007" y="11.666666666666664" width="97.666666666666686" height="21"/>
|
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
|
||||||
<color key="textColor" red="0.95294117647058818" green="0.97647058823529409" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
</subviews>
|
|
||||||
<color key="backgroundColor" red="0.1215686275" green="0.12941176469999999" blue="0.14117647059999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="b1d-b4-TCK" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="pxI-vz-oR6"/>
|
|
||||||
<constraint firstItem="b1d-b4-TCK" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="xCR-Og-ne7"/>
|
|
||||||
</constraints>
|
|
||||||
</tableViewCellContentView>
|
|
||||||
<viewLayoutGuide key="safeArea" id="aW0-zy-SZf"/>
|
|
||||||
<color key="backgroundColor" red="0.1215686275" green="0.12941176469999999" blue="0.14117647059999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
<color key="tintColor" red="0.6705882353" green="0.61568627450000002" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
<connections>
|
|
||||||
<outlet property="name" destination="b1d-b4-TCK" id="CX8-IC-jlM"/>
|
|
||||||
</connections>
|
|
||||||
<point key="canvasLocation" x="108" y="39"/>
|
|
||||||
</tableViewCell>
|
|
||||||
</objects>
|
|
||||||
</document>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
//
|
|
||||||
// LayoutOnScreenControlsViewController.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Long Le on 9/27/22.
|
|
||||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
#import "LayoutOnScreenControls.h"
|
|
||||||
#import "ToolBarContainerView.h"
|
|
||||||
#import "OSCProfilesManager.h"
|
|
||||||
#import "OSCProfilesTableViewController.h"
|
|
||||||
#import "Moonlight-Swift.h"
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
/**
|
|
||||||
This view controller provides the user interface which allows the user to position on screen controller buttons anywhere they'd like on the screen. It also provides the user with the abilities to undo a change, save the on screen controller layout for later retrieval, and load previously saved controller layouts
|
|
||||||
*/
|
|
||||||
@interface LayoutOnScreenControlsViewController : UIViewController
|
|
||||||
- (void)profileRefresh;
|
|
||||||
- (void)reloadOnScreenWidgetViews;
|
|
||||||
|
|
||||||
@property LayoutOnScreenControls *layoutOSC; // object that contains a view which contains the on screen controller buttons that allows the user to drag and positions each button on the screen using touch
|
|
||||||
@property (nonatomic) NSMutableSet* OnScreenWidgetViews;
|
|
||||||
|
|
||||||
@property int OSCSegmentSelected;
|
|
||||||
|
|
||||||
@property (weak, nonatomic) IBOutlet UIButton *trashCanButton;
|
|
||||||
@property (weak, nonatomic) IBOutlet UIButton *undoButton;
|
|
||||||
|
|
||||||
@property (weak, nonatomic) IBOutlet ToolBarContainerView *toolbarRootView;
|
|
||||||
@property (weak, nonatomic) IBOutlet UIView *chevronView;
|
|
||||||
@property (weak, nonatomic) IBOutlet UIImageView *chevronImageView;
|
|
||||||
@property (weak, nonatomic) IBOutlet UIStackView *toolbarStackView;
|
|
||||||
@property (strong, nonatomic) OSCProfilesTableViewController *oscProfilesTableViewController;
|
|
||||||
@property (nonatomic, assign) NSString *currentProfileName;
|
|
||||||
@property (strong, nonatomic) IBOutlet UILabel *currentProfileLabel;
|
|
||||||
@property (strong, nonatomic) IBOutlet UISlider *widgetSizeSlider;
|
|
||||||
@property (strong, nonatomic) IBOutlet UISlider *widgetHeightSlider;
|
|
||||||
@property (strong, nonatomic) IBOutlet UISlider *widgetAlphaSlider;
|
|
||||||
@property (strong, nonatomic) IBOutlet UISlider *sensitivityFactorSlider;
|
|
||||||
@property (strong, nonatomic) IBOutlet UISlider *stickIndicatorOffsetSlider;
|
|
||||||
@property (strong, nonatomic) IBOutlet UISlider *widgetBorderWidthSlider;
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,878 +0,0 @@
|
|||||||
//
|
|
||||||
// LayoutOnScreenControlsViewController.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Long Le on 9/27/22.
|
|
||||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "LayoutOnScreenControlsViewController.h"
|
|
||||||
#import "OSCProfilesTableViewController.h"
|
|
||||||
#import "OnScreenButtonState.h"
|
|
||||||
//#import "OnScreenControls.h"
|
|
||||||
#import "OSCProfilesManager.h"
|
|
||||||
#import "LocalizationHelper.h"
|
|
||||||
#import "Moonlight-Swift.h"
|
|
||||||
|
|
||||||
@interface LayoutOnScreenControlsViewController ()
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
|
|
||||||
@implementation LayoutOnScreenControlsViewController {
|
|
||||||
BOOL isToolbarHidden;
|
|
||||||
OSCProfilesManager* profilesManager;
|
|
||||||
OnScreenWidgetView* selectedWidgetView;
|
|
||||||
CALayer* selectedControllerLayer;
|
|
||||||
CGRect controllerLoadedBounds;
|
|
||||||
bool widgetViewSelected;
|
|
||||||
bool controllerLayerSelected;
|
|
||||||
__weak IBOutlet NSLayoutConstraint *toolbarTopConstraintiPhone;
|
|
||||||
__weak IBOutlet NSLayoutConstraint *toolbarTopConstraintiPad;
|
|
||||||
UILabel *widgetSizeSliderLabel;
|
|
||||||
UILabel *widgetHeightSliderLabel;
|
|
||||||
UILabel *widgetAlphaSliderLabel;
|
|
||||||
UILabel *widgetBorderWidthSliderLabel;
|
|
||||||
UILabel *sensitivitySliderLabel;
|
|
||||||
UILabel *stickIndicatorOffsetSliderLabel;
|
|
||||||
UILabel *stickIndicatorOffsetExplain;
|
|
||||||
}
|
|
||||||
|
|
||||||
@synthesize trashCanButton;
|
|
||||||
@synthesize undoButton;
|
|
||||||
@synthesize OSCSegmentSelected;
|
|
||||||
@synthesize toolbarRootView;
|
|
||||||
@synthesize chevronView;
|
|
||||||
@synthesize chevronImageView;
|
|
||||||
|
|
||||||
- (UIInterfaceOrientationMask)getCurrentOrientation{
|
|
||||||
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]);
|
|
||||||
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
|
|
||||||
//lock the orientation accordingly after streaming is started
|
|
||||||
if(screenWidthInPoints > screenHeightInPoints) return UIInterfaceOrientationMaskLandscape;
|
|
||||||
else return UIInterfaceOrientationMaskPortrait|UIInterfaceOrientationMaskPortraitUpsideDown;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
|
|
||||||
// Return the supported interface orientations acoordingly
|
|
||||||
return [self getCurrentOrientation]; // 90 Degree rotation not allowed in streaming or app view
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) viewWillDisappear:(BOOL)animated{
|
|
||||||
OnScreenWidgetView.editMode = false;
|
|
||||||
for (OnScreenWidgetView* widgetView in self.OnScreenWidgetViews){
|
|
||||||
[widgetView.stickBallLayer removeFromSuperlayer];
|
|
||||||
[widgetView.crossMarkLayer removeFromSuperlayer];
|
|
||||||
}
|
|
||||||
[super viewWillDisappear:animated];
|
|
||||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"OscLayoutCloseNotification" object:self];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) reloadOnScreenWidgetViews {
|
|
||||||
for (UIView *subview in self.view.subviews) {
|
|
||||||
// 检查子视图是否是特定类型的实例
|
|
||||||
if ([subview isKindOfClass:[OnScreenWidgetView class]]) {
|
|
||||||
// 如果是,就添加到将要被移除的数组中
|
|
||||||
[subview removeFromSuperview];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[self.OnScreenWidgetViews removeAllObjects];
|
|
||||||
|
|
||||||
NSLog(@"reload os Key here");
|
|
||||||
|
|
||||||
// _activeCustomOscButtonPositionDict will be updated every time when the osc profile is reloaded
|
|
||||||
OSCProfile *oscProfile = [profilesManager getSelectedProfile]; //returns the currently selected OSCProfile
|
|
||||||
for (NSData *buttonStateEncoded in oscProfile.buttonStates) {
|
|
||||||
OnScreenButtonState* buttonState = [NSKeyedUnarchiver unarchivedObjectOfClass:[OnScreenButtonState class] fromData:buttonStateEncoded error:nil];
|
|
||||||
if(buttonState.buttonType == CustomOnScreenWidget){
|
|
||||||
OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithKeyString:buttonState.name keyLabel:buttonState.alias]; //reconstruct widgetView
|
|
||||||
widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place
|
|
||||||
widgetView.timestamp = buttonState.timestamp; // will be set as key in in the dict.
|
|
||||||
widgetView.widthFactor = buttonState.widthFactor;
|
|
||||||
widgetView.heightFactor = buttonState.heightFactor;
|
|
||||||
widgetView.borderWidth = buttonState.borderWidth;
|
|
||||||
widgetView.sensitivityFactor = buttonState.sensitivityFactor;
|
|
||||||
widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset;
|
|
||||||
// widgetView.backgroundAlpha = buttonState.backgroundAlpha;
|
|
||||||
// Add the widgetView to the view controller's view
|
|
||||||
[self.view addSubview:widgetView];
|
|
||||||
[widgetView setLocationWithXOffset:buttonState.position.x yOffset:buttonState.position.y];
|
|
||||||
[widgetView resizeWidgetView]; // resize must be called after relocation
|
|
||||||
[widgetView adjustTransparencyWithAlpha:buttonState.backgroundAlpha];
|
|
||||||
[widgetView adjustBorderWithWidth:buttonState.borderWidth];
|
|
||||||
[self.OnScreenWidgetViews addObject:widgetView];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void) viewDidLoad {
|
|
||||||
[super viewDidLoad];
|
|
||||||
profilesManager = [OSCProfilesManager sharedManager];
|
|
||||||
self.OnScreenWidgetViews = [[NSMutableSet alloc] init]; // will be revised to read persisted data , somewhere else
|
|
||||||
[OSCProfilesManager setOnScreenWidgetViewsSet:self.OnScreenWidgetViews]; // pass the keyboard button dict to profiles manager
|
|
||||||
|
|
||||||
isToolbarHidden = NO; // keeps track if the toolbar is hidden up above the screen so that we know whether to hide or show it when the user taps the toolbar's hide/show button
|
|
||||||
|
|
||||||
widgetSizeSliderLabel = [[UILabel alloc] init];
|
|
||||||
widgetHeightSliderLabel = [[UILabel alloc] init];
|
|
||||||
widgetAlphaSliderLabel = [[UILabel alloc] init];
|
|
||||||
widgetBorderWidthSliderLabel = [[UILabel alloc] init];
|
|
||||||
sensitivitySliderLabel = [[UILabel alloc] init];
|
|
||||||
stickIndicatorOffsetSliderLabel = [[UILabel alloc] init];
|
|
||||||
stickIndicatorOffsetExplain = [[UILabel alloc] init];
|
|
||||||
|
|
||||||
/* add curve to bottom of chevron tab view */
|
|
||||||
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.chevronView.bounds byRoundingCorners:(UIRectCornerBottomLeft | UIRectCornerBottomRight) cornerRadii:CGSizeMake(10.0, 10.0)];
|
|
||||||
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
|
|
||||||
maskLayer.frame = self.view.bounds;
|
|
||||||
maskLayer.path = maskPath.CGPath;
|
|
||||||
self.chevronView.layer.mask = maskLayer;
|
|
||||||
|
|
||||||
/* Add swipe gesture to toolbar to allow user to swipe it up and off screen */
|
|
||||||
UISwipeGestureRecognizer *swipeUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(moveToolbar:)];
|
|
||||||
swipeUp.direction = UISwipeGestureRecognizerDirectionUp;
|
|
||||||
[self.toolbarRootView addGestureRecognizer:swipeUp];
|
|
||||||
|
|
||||||
/* Add tap gesture to toolbar's chevron to allow user to tap it in order to move the toolbar on and off screen */
|
|
||||||
UITapGestureRecognizer *singleFingerTap =
|
|
||||||
[[UITapGestureRecognizer alloc] initWithTarget:self
|
|
||||||
action:@selector(moveToolbar:)];
|
|
||||||
[self.chevronView addGestureRecognizer:singleFingerTap];
|
|
||||||
|
|
||||||
self.layoutOSC = [[LayoutOnScreenControls alloc] initWithView:self.view controllerSup:nil streamConfig:nil oscLevel:OSCSegmentSelected];
|
|
||||||
self.layoutOSC._level = OnScreenControlsLevelCustom;
|
|
||||||
self.layoutOSC.layoutToolVC = self;
|
|
||||||
[self.layoutOSC show]; // draw on screen controls
|
|
||||||
|
|
||||||
[self addInnerAnalogSticksToOuterAnalogLayers]; // allows inner and analog sticks to be dragged together around the screen together as one unit which is the expected behavior
|
|
||||||
|
|
||||||
self.undoButton.alpha = 0.3; // no changes to undo yet, so fade out the undo button a bit
|
|
||||||
|
|
||||||
if ([[profilesManager getAllProfiles] count] == 0) { // if no saved OSC profiles exist yet then create one called 'Default' and associate it with Moonlight's legacy 'Full' OSC layout that's already been laid out on the screen at this point
|
|
||||||
[profilesManager saveProfileWithName:@"Default" andButtonLayers:self.layoutOSC.OSCButtonLayers];
|
|
||||||
}
|
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
||||||
selector:@selector(clearSelectedWidgetView)
|
|
||||||
name:@"LegacyOscCALayerSelectedNotification"
|
|
||||||
object:nil];
|
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
||||||
selector:@selector(profileRefresh)
|
|
||||||
name:@"OscLayoutTableViewCloseNotification"
|
|
||||||
object:nil];
|
|
||||||
|
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
||||||
selector:@selector(reloadOnScreenWidgetViews)
|
|
||||||
name:@"OscLayoutProfileSelctedInTableView" // This is a special notification for reloading the on screen keyboard buttons. which can't be executed by _oscProfilesTableViewController.needToUpdateOscLayoutTVC code block, and has to be triggered by a notification
|
|
||||||
object:nil];
|
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
||||||
selector:@selector(widgetViewTapped:)
|
|
||||||
name:@"OnScreenWidgetViewSelected"
|
|
||||||
object:nil];
|
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
||||||
selector:@selector(setControllerCALayerSliderValues:)
|
|
||||||
name:@"ControllerCALayerSelected"
|
|
||||||
object:nil];
|
|
||||||
|
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(OSCLayoutChanged) name:@"OSCLayoutChanged" object:nil]; // used to notifiy this view controller that the user made a change to the OSC layout so that the VC can either fade in or out its 'Undo button' which will signify to the user whether there are any OSC layout changes to undo
|
|
||||||
|
|
||||||
/* This will animate the toolbar with a subtle up and down motion intended to telegraph to the user that they can hide the toolbar if they wish*/
|
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
||||||
|
|
||||||
[UIView animateWithDuration:0.3
|
|
||||||
delay:0.25
|
|
||||||
usingSpringWithDamping:0.8
|
|
||||||
initialSpringVelocity:0.5
|
|
||||||
options:UIViewAnimationOptionCurveEaseInOut animations:^{ // Animate toolbar up a a very small distance. Note the 0.35 time delay is necessary to avoid a bug that keeps animations from playing if the animation is presented immediately on a modally presented VC
|
|
||||||
self.toolbarRootView.frame = CGRectMake(self.toolbarRootView.frame.origin.x, self.toolbarRootView.frame.origin.y - 25, self.toolbarRootView.frame.size.width, self.toolbarRootView.frame.size.height);
|
|
||||||
}
|
|
||||||
completion:^(BOOL finished) {
|
|
||||||
[UIView animateWithDuration:0.3
|
|
||||||
delay:0
|
|
||||||
usingSpringWithDamping:0.7
|
|
||||||
initialSpringVelocity:1.0
|
|
||||||
options:UIViewAnimationOptionCurveEaseIn animations:^{ // Animate the toolbar back down that same distance
|
|
||||||
self.toolbarRootView.frame = CGRectMake(self.toolbarRootView.frame.origin.x, self.toolbarRootView.frame.origin.y + 25, self.toolbarRootView.frame.size.width, self.toolbarRootView.frame.size.height);
|
|
||||||
}
|
|
||||||
completion:^(BOOL finished) {
|
|
||||||
NSLog (@"done");
|
|
||||||
}];
|
|
||||||
}];
|
|
||||||
});
|
|
||||||
[self profileRefresh];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- (void) viewDidAppear:(BOOL)animated {
|
|
||||||
OnScreenWidgetView.editMode = true;
|
|
||||||
[super viewWillAppear:animated];
|
|
||||||
[self profileRefresh];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Class Helper Functions
|
|
||||||
|
|
||||||
/* fades the 'Undo Button' in or out depending on whether the user has any OSC layout changes to undo */
|
|
||||||
- (void) OSCLayoutChanged {
|
|
||||||
if ([self.layoutOSC.layoutChanges count] > 0) {
|
|
||||||
self.undoButton.alpha = 1.0;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.undoButton.alpha = 0.3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* animates the toolbar up and off the screen or back down onto the screen */
|
|
||||||
- (void) moveToolbar:(UISwipeGestureRecognizer *)sender {
|
|
||||||
BOOL isPad = [[UIDevice currentDevice].model hasPrefix:@"iPad"];
|
|
||||||
NSLayoutConstraint *toolbarTopConstraint = isPad ? self->toolbarTopConstraintiPad : self->toolbarTopConstraintiPhone;
|
|
||||||
if (isToolbarHidden == NO) {
|
|
||||||
[UIView animateWithDuration:0.2 animations:^{ // animates toolbar up and off screen
|
|
||||||
toolbarTopConstraint.constant -= self.toolbarRootView.frame.size.height;
|
|
||||||
[self.view layoutIfNeeded];
|
|
||||||
|
|
||||||
}
|
|
||||||
completion:^(BOOL finished) {
|
|
||||||
if (finished) {
|
|
||||||
self->isToolbarHidden = YES;
|
|
||||||
self.chevronImageView.image = [UIImage imageNamed:@"ChevronCompactDown"];
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[UIView animateWithDuration:0.2 animations:^{ // animates the toolbar back down into the screen
|
|
||||||
toolbarTopConstraint.constant += self.toolbarRootView.frame.size.height;
|
|
||||||
[self.view layoutIfNeeded];
|
|
||||||
}
|
|
||||||
completion:^(BOOL finished) {
|
|
||||||
if (finished) {
|
|
||||||
self->isToolbarHidden = NO;
|
|
||||||
self.chevronImageView.image = [UIImage imageNamed:@"ChevronCompactUp"];
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes the inner analog stick layers a child layer of its corresponding outer analog stick layers so that both the inner and its corresponding outer layers move together when the user drags them around the screen as is the expected behavior when laying out OSC. Note that this is NOT expected behavior on the game stream view where the inner analog sticks move to follow toward the user's touch and their corresponding outer analog stick layers do not move
|
|
||||||
*/
|
|
||||||
- (void)addInnerAnalogSticksToOuterAnalogLayers {
|
|
||||||
// right stick
|
|
||||||
[self.layoutOSC._rightStickBackground addSublayer: self.layoutOSC._rightStick];
|
|
||||||
self.layoutOSC._rightStick.position = CGPointMake(self.layoutOSC._rightStickBackground.frame.size.width / 2, self.layoutOSC._rightStickBackground.frame.size.height / 2);
|
|
||||||
|
|
||||||
// left stick
|
|
||||||
[self.layoutOSC._leftStickBackground addSublayer: self.layoutOSC._leftStick];
|
|
||||||
self.layoutOSC._leftStick.position = CGPointMake(self.layoutOSC._leftStickBackground.frame.size.width / 2, self.layoutOSC._leftStickBackground.frame.size.height / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - UIButton Actions
|
|
||||||
|
|
||||||
- (IBAction) closeTapped:(id)sender {
|
|
||||||
[self dismissViewControllerAnimated:YES completion:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (IBAction) trashCanTapped:(id)sender {
|
|
||||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Delete Buttons Here"] message:[LocalizationHelper localizedStringForKey:@"Drag and drop buttons onto this trash can to remove them from the interface"] preferredStyle:UIAlertControllerStyleAlert];
|
|
||||||
|
|
||||||
UIAlertAction *ok = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:nil];
|
|
||||||
[alert addAction:ok];
|
|
||||||
[self presentViewController:alert animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (IBAction) undoTapped:(id)sender {
|
|
||||||
if ([self.layoutOSC.layoutChanges count] > 0) { // check if there are layout changes to roll back to
|
|
||||||
OnScreenButtonState *buttonState = [self.layoutOSC.layoutChanges lastObject]; // Get the 'OnScreenButtonState' object that contains the name, position, and visiblity state of the button the user last moved
|
|
||||||
|
|
||||||
CALayer *buttonLayer = [self.layoutOSC controllerLayerFromName:buttonState.name]; // get the on screen button layer that corresponds with the 'OnScreenButtonState' object that we retrieved above
|
|
||||||
|
|
||||||
/* Set the button's position and visiblity to what it was before the user last moved it */
|
|
||||||
buttonLayer.position = buttonState.position;
|
|
||||||
buttonLayer.hidden = buttonState.isHidden;
|
|
||||||
|
|
||||||
/* if user is showing or hiding dPad, then show or hide all four dPad button child layers as well since setting the 'hidden' property on the parent CALayer is not automatically setting the individual dPad child CALayers */
|
|
||||||
if ([buttonLayer.name isEqualToString:@"dPad"]) {
|
|
||||||
self.layoutOSC._upButton.hidden = buttonState.isHidden;
|
|
||||||
self.layoutOSC._rightButton.hidden = buttonState.isHidden;
|
|
||||||
self.layoutOSC._downButton.hidden = buttonState.isHidden;
|
|
||||||
self.layoutOSC._leftButton.hidden = buttonState.isHidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* if user is showing or hiding the left or right analog sticks, then show or hide their corresponding inner analog stick child layers as well since setting the 'hidden' property on the parent analog stick doesn't automatically hide its child inner analog stick CALayer */
|
|
||||||
if ([buttonLayer.name isEqualToString:@"leftStickBackground"]) {
|
|
||||||
self.layoutOSC._leftStick.hidden = buttonState.isHidden;
|
|
||||||
}
|
|
||||||
if ([buttonLayer.name isEqualToString:@"rightStickBackground"]) {
|
|
||||||
self.layoutOSC._rightStick.hidden = buttonState.isHidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
[self.layoutOSC.layoutChanges removeLastObject];
|
|
||||||
|
|
||||||
[self OSCLayoutChanged]; // will fade the undo button in or out depending on whether there are any further changes to undo
|
|
||||||
}
|
|
||||||
else { // there are no changes to undo. let user know there are no changes to undo
|
|
||||||
UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [LocalizationHelper localizedStringForKey:@"Nothing to Undo"] message: [LocalizationHelper localizedStringForKey: @"There are no changes to undo"] preferredStyle:UIAlertControllerStyleAlert];
|
|
||||||
[savedAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
|
||||||
[savedAlertController dismissViewControllerAnimated:NO completion:nil];
|
|
||||||
}]];
|
|
||||||
[self presentViewController:savedAlertController animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (IBAction) addTapped:(id)sender{
|
|
||||||
|
|
||||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"New On-Screen Widget"]
|
|
||||||
message:[LocalizationHelper localizedStringForKey:@"Enter the command & alias label"]
|
|
||||||
preferredStyle:UIAlertControllerStyleAlert];
|
|
||||||
|
|
||||||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
|
||||||
textField.placeholder = [LocalizationHelper localizedStringForKey:@"Command"];
|
|
||||||
textField.keyboardType = UIKeyboardTypeASCIICapable;
|
|
||||||
textField.autocorrectionType = UITextAutocorrectionTypeNo;
|
|
||||||
textField.spellCheckingType = UITextSpellCheckingTypeNo;
|
|
||||||
}];
|
|
||||||
|
|
||||||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
|
||||||
textField.placeholder = [LocalizationHelper localizedStringForKey:@"Alias label (optional)"];
|
|
||||||
textField.keyboardType = UIKeyboardTypeASCIICapable;
|
|
||||||
textField.autocorrectionType = UITextAutocorrectionTypeNo;
|
|
||||||
textField.spellCheckingType = UITextSpellCheckingTypeNo;
|
|
||||||
}];
|
|
||||||
|
|
||||||
|
|
||||||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Cancel"]
|
|
||||||
style:UIAlertActionStyleCancel
|
|
||||||
handler:nil];
|
|
||||||
|
|
||||||
UIAlertAction *okAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"OK"]
|
|
||||||
style:UIAlertActionStyleDefault
|
|
||||||
handler:^(UIAlertAction *action) {
|
|
||||||
/*
|
|
||||||
alertController.textFields[0].keyboardType = UIKeyboardTypeASCIICapable;
|
|
||||||
alertController.textFields[0].autocorrectionType = UITextAutocorrectionTypeNo;
|
|
||||||
alertController.textFields[0].spellCheckingType = UITextSpellCheckingTypeNo;
|
|
||||||
alertController.textFields[1].keyboardType = UIKeyboardTypeASCIICapable;
|
|
||||||
alertController.textFields[1].autocorrectionType = UITextAutocorrectionTypeNo;
|
|
||||||
alertController.textFields[1].spellCheckingType = UITextSpellCheckingTypeNo;*/
|
|
||||||
|
|
||||||
NSString *cmdString = [alertController.textFields[0].text uppercaseString]; // convert to uppercase
|
|
||||||
NSString *keyLabel = alertController.textFields[1].text;
|
|
||||||
if([keyLabel isEqualToString:@""]) keyLabel = [[cmdString lowercaseString] capitalizedString];
|
|
||||||
bool noValidKeyboardString = [CommandManager.shared extractKeyStringsFromComboCommandFrom:cmdString] == nil; // this is a invalid string.
|
|
||||||
bool noValidSuperComboButtonString = [CommandManager.shared extractKeyStringsFromComboKeysFrom:cmdString] == nil; // this is a invalid string.
|
|
||||||
bool noValidMouseButtonString = ![CommandManager.mouseButtonMappings.allKeys containsObject:cmdString];
|
|
||||||
bool noValidTouchPadString = ![CommandManager.touchPadCmds containsObject:cmdString];
|
|
||||||
bool noValidOscButtonString = ![CommandManager.oscButtonMappings.allKeys containsObject:cmdString];
|
|
||||||
bool noValidSpecialButtonString = ![CommandManager.specialOverlayButtonCmds containsObject:cmdString];
|
|
||||||
|
|
||||||
if(noValidKeyboardString && noValidMouseButtonString && noValidTouchPadString && noValidOscButtonString && noValidSpecialButtonString && noValidSuperComboButtonString) return;
|
|
||||||
|
|
||||||
//saving & present the keyboard button:
|
|
||||||
OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithKeyString:cmdString keyLabel:keyLabel];
|
|
||||||
widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place
|
|
||||||
widgetView.timestamp = CACurrentMediaTime(); // will be set as key in in the dict.
|
|
||||||
[self.OnScreenWidgetViews addObject:widgetView];
|
|
||||||
// Add the widgetView to the view controller's view
|
|
||||||
[self.view addSubview:widgetView];
|
|
||||||
[widgetView setLocationWithXOffset:50 yOffset:50];
|
|
||||||
[widgetView resizeWidgetView];
|
|
||||||
}];
|
|
||||||
[alertController addAction:cancelAction];
|
|
||||||
[alertController addAction:okAction];
|
|
||||||
[self presentViewController:alertController animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* show pop up notification that lets users choose to save the current OSC layout configuration as a profile they can load when they want. User can also choose to cancel out of this pop up */
|
|
||||||
- (IBAction) saveTapped:(id)sender {
|
|
||||||
|
|
||||||
if([self->profilesManager updateSelectedProfile:self.layoutOSC.OSCButtonLayers]){
|
|
||||||
UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Current profile updated successfully"] preferredStyle:UIAlertControllerStyleAlert];
|
|
||||||
[savedAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
|
||||||
}]];
|
|
||||||
[self presentViewController:savedAlertController animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Profile Default can not be overwritten"] preferredStyle:UIAlertControllerStyleAlert];
|
|
||||||
[savedAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
|
||||||
[self.oscProfilesTableViewController profileViewRefresh]; // execute this will reset layout in OSC tool!
|
|
||||||
}]];
|
|
||||||
[self presentViewController:savedAlertController animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)clearSelectedWidgetView{
|
|
||||||
selectedWidgetView = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)widgetViewTapped: (NSNotification *)notification{
|
|
||||||
// receive the selected widgetView obj passed from the notification
|
|
||||||
OnScreenWidgetView* widgetView = (OnScreenWidgetView* )notification.object;
|
|
||||||
self->widgetViewSelected = true;
|
|
||||||
self->controllerLayerSelected = false;
|
|
||||||
self->selectedWidgetView = widgetView;
|
|
||||||
// setup slider values
|
|
||||||
[self.widgetSizeSlider setValue: self->selectedWidgetView.widthFactor];
|
|
||||||
[self.widgetHeightSlider setValue: self->selectedWidgetView.heightFactor];
|
|
||||||
[self.widgetAlphaSlider setValue: self->selectedWidgetView.backgroundAlpha];
|
|
||||||
[self.widgetBorderWidthSlider setValue:self->selectedWidgetView.borderWidth];
|
|
||||||
|
|
||||||
NSSet *stickAndMouseTouchpads = [NSSet setWithObjects:@"LSPAD", @"RSPAD", @"LSVPAD", @"RSVPAD", @"MOUSEPAD", nil];
|
|
||||||
NSSet *nonVectorStickPads = [NSSet setWithObjects:@"LSPAD", @"RSPAD", nil];
|
|
||||||
|
|
||||||
|
|
||||||
bool showSensitivityFactorSlider = [stickAndMouseTouchpads containsObject:self->selectedWidgetView.keyString];
|
|
||||||
bool showStickIndicatorOffsetSlider = [nonVectorStickPads containsObject:self->selectedWidgetView.keyString];
|
|
||||||
self.sensitivityFactorSlider.hidden = self->sensitivitySliderLabel.hidden = !showSensitivityFactorSlider;
|
|
||||||
self.stickIndicatorOffsetSlider.hidden = self->stickIndicatorOffsetExplain.hidden = self->stickIndicatorOffsetSliderLabel.hidden = !showStickIndicatorOffsetSlider;
|
|
||||||
if(showSensitivityFactorSlider){
|
|
||||||
[self.sensitivityFactorSlider setValue:self->selectedWidgetView.sensitivityFactor];
|
|
||||||
[sensitivitySliderLabel setText:[LocalizationHelper localizedStringForKey:@"Sensitivity: %.2f", self->selectedWidgetView.sensitivityFactor]];
|
|
||||||
}
|
|
||||||
if(showStickIndicatorOffsetSlider){
|
|
||||||
// illustrating the indicator offset,
|
|
||||||
[selectedWidgetView.stickBallLayer removeFromSuperlayer];
|
|
||||||
[selectedWidgetView.crossMarkLayer removeFromSuperlayer];
|
|
||||||
selectedWidgetView.touchBeganLocation = CGPointMake(CGRectGetWidth(selectedWidgetView.frame)/2, CGRectGetHeight(selectedWidgetView.frame)/4);
|
|
||||||
[selectedWidgetView showStickIndicator];// this will create the indicator CAShapeLayers
|
|
||||||
[self.stickIndicatorOffsetSlider setValue:self->selectedWidgetView.stickIndicatorOffset];
|
|
||||||
[stickIndicatorOffsetSliderLabel setText:[LocalizationHelper localizedStringForKey:@"Indicator Offset: %.2f", self->selectedWidgetView.stickIndicatorOffset]];
|
|
||||||
[self->selectedWidgetView updateStickIndicator];
|
|
||||||
}
|
|
||||||
[widgetSizeSliderLabel setText:[LocalizationHelper localizedStringForKey:@"Widget Size: %.2f", self->selectedWidgetView.widthFactor]];
|
|
||||||
[widgetHeightSliderLabel setText:[LocalizationHelper localizedStringForKey:@"Widget Height: %.2f", self->selectedWidgetView.heightFactor]];
|
|
||||||
[widgetAlphaSliderLabel setText:[LocalizationHelper localizedStringForKey:@"Widget Alpha: %.2f", self->selectedWidgetView.backgroundAlpha]];
|
|
||||||
[widgetBorderWidthSliderLabel setText:[LocalizationHelper localizedStringForKey:@"Border Width: %.2f", self->selectedWidgetView.borderWidth]];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setControllerCALayerSliderValues: (NSNotification *)notification{
|
|
||||||
// receive the selected widgetView obj passed from the notification
|
|
||||||
CALayer* controllerLayer = (CALayer* )notification.object;
|
|
||||||
self->widgetViewSelected = false;
|
|
||||||
self->stickIndicatorOffsetExplain.hidden = true;
|
|
||||||
self->stickIndicatorOffsetSliderLabel.hidden = true;
|
|
||||||
self.stickIndicatorOffsetSlider.hidden = true;
|
|
||||||
self->sensitivitySliderLabel.hidden = true;
|
|
||||||
self.sensitivityFactorSlider.hidden = true;
|
|
||||||
|
|
||||||
self->controllerLayerSelected = true;
|
|
||||||
self->selectedControllerLayer = controllerLayer;
|
|
||||||
self->controllerLoadedBounds = controllerLayer.bounds;
|
|
||||||
|
|
||||||
// setup slider values
|
|
||||||
CGFloat sizeFactor = [OnScreenControls getControllerLayerSizeFactor:controllerLayer]; // calculated sizeFactor from loaded layer bounds.
|
|
||||||
[self.widgetSizeSlider setValue:sizeFactor];
|
|
||||||
[self.widgetHeightSlider setValue:sizeFactor];
|
|
||||||
CGFloat alpha = [self.layoutOSC getControllerLayerOpacity:controllerLayer];
|
|
||||||
[self.widgetAlphaSlider setValue:alpha];
|
|
||||||
|
|
||||||
[widgetSizeSliderLabel setText:[LocalizationHelper localizedStringForKey:@"Widget Size: %.2f", sizeFactor]];
|
|
||||||
[widgetHeightSliderLabel setText:[LocalizationHelper localizedStringForKey:@"Widget Height: %.2f", sizeFactor]];
|
|
||||||
[widgetAlphaSliderLabel setText:[LocalizationHelper localizedStringForKey:@"Widget Alpha: %.2f", alpha]];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)widgetSizeSliderMoved{
|
|
||||||
[widgetSizeSliderLabel setText:[LocalizationHelper localizedStringForKey:@"Widget Size: %.2f", self.widgetSizeSlider.value]];
|
|
||||||
[widgetHeightSliderLabel setText:[LocalizationHelper localizedStringForKey:@"Widget Height: %.2f", self.widgetSizeSlider.value]]; // resizing the whole button
|
|
||||||
[self.widgetHeightSlider setValue: self.widgetSizeSlider.value];
|
|
||||||
|
|
||||||
if(self->selectedWidgetView != nil && self->widgetViewSelected){
|
|
||||||
self->selectedWidgetView.translatesAutoresizingMaskIntoConstraints = true; // this is mandatory to prevent unexpected key view location change
|
|
||||||
// when adjusting width, the widgetView height will be syncronized
|
|
||||||
self->selectedWidgetView.widthFactor = self->selectedWidgetView.heightFactor = self.widgetSizeSlider.value;
|
|
||||||
[self->selectedWidgetView resizeWidgetView];
|
|
||||||
}
|
|
||||||
if(self->selectedControllerLayer != nil && self->controllerLayerSelected){
|
|
||||||
[self.layoutOSC resizeControllerLayerWith:self->selectedControllerLayer and:self.widgetSizeSlider.value];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)widgetHeightSliderMoved{
|
|
||||||
[widgetHeightSliderLabel setText:[LocalizationHelper localizedStringForKey:@"Widget Height: %.2f", self.widgetHeightSlider.value]];
|
|
||||||
if(self->selectedWidgetView != nil && self->widgetViewSelected){
|
|
||||||
self->selectedWidgetView.translatesAutoresizingMaskIntoConstraints = true; // this is mandatory to prevent unexpected key view location change
|
|
||||||
if([CommandManager.oscButtonMappings.allKeys containsObject:selectedWidgetView.keyString] && ![CommandManager.oscRectangleButtonCmds containsObject:selectedWidgetView.keyString]) return; // don't change custom osc button height, except for dPad buttons which are in rectangle shape
|
|
||||||
self->selectedWidgetView.heightFactor = self.widgetHeightSlider.value;
|
|
||||||
[self->selectedWidgetView resizeWidgetView];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)widgetAlphaSliderMoved{
|
|
||||||
[widgetAlphaSliderLabel setText:[LocalizationHelper localizedStringForKey:@"Widget Alpha: %.2f", self.widgetAlphaSlider.value]];
|
|
||||||
if(self->selectedWidgetView != nil && self->widgetViewSelected){
|
|
||||||
[self->selectedWidgetView adjustTransparencyWithAlpha:self.widgetAlphaSlider.value];
|
|
||||||
}
|
|
||||||
|
|
||||||
if(self->selectedControllerLayer != nil && self->controllerLayerSelected){
|
|
||||||
[self.layoutOSC adjustControllerLayerOpacityWith:self->selectedControllerLayer and:self.widgetAlphaSlider.value];
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)widgetBorderWidthSliderMoved{
|
|
||||||
[widgetBorderWidthSliderLabel setText:[LocalizationHelper localizedStringForKey:@"Border Width: %.2f", self.widgetBorderWidthSlider.value]];
|
|
||||||
if(self->selectedWidgetView != nil && self->widgetViewSelected){
|
|
||||||
[self->selectedWidgetView adjustBorderWithWidth:self.widgetBorderWidthSlider.value];
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)sensitivitySliderMoved{
|
|
||||||
[sensitivitySliderLabel setText:[LocalizationHelper localizedStringForKey:@"Sensitivity: %.2f", self.sensitivityFactorSlider.value]];
|
|
||||||
if(self->selectedWidgetView != nil && self->widgetViewSelected) self->selectedWidgetView.sensitivityFactor = self.sensitivityFactorSlider.value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)stickIndicatorOffsetSliderMoved{
|
|
||||||
[stickIndicatorOffsetSliderLabel setText:[LocalizationHelper localizedStringForKey:@"Indicator Offset: %.2f", self.stickIndicatorOffsetSlider.value]];
|
|
||||||
if(self->selectedWidgetView != nil && self->widgetViewSelected){
|
|
||||||
self->selectedWidgetView.stickIndicatorOffset = self.stickIndicatorOffsetSlider.value;
|
|
||||||
[self->selectedWidgetView updateStickIndicator];
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)showIndicatorOffset{
|
|
||||||
selectedWidgetView.touchBeganLocation = CGPointMake(CGRectGetWidth(selectedWidgetView.frame)/2, CGRectGetHeight(selectedWidgetView.frame)/4);
|
|
||||||
[selectedWidgetView showStickIndicator];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setupProfileLableAndSliders{
|
|
||||||
// self.currentProfileLabel.frame = CGRectMake(0, 0, 180, 35);
|
|
||||||
// CGFloat profileLabelXPosition = self.view.bounds.size.width - self.currentProfileLabel.frame.size.width - 20;
|
|
||||||
CGFloat sliderXPosition = (self.view.bounds.size.width - self.widgetSizeSlider.frame.size.width)/2 ;
|
|
||||||
CGFloat profileLabelXPosition = self.view.bounds.size.width/2 + (self.widgetSizeSlider.frame.size.width)/2 + 8;
|
|
||||||
|
|
||||||
// Set the label's frame with the calculated x-position
|
|
||||||
self.currentProfileLabel.frame = CGRectMake(profileLabelXPosition, self.currentProfileLabel.frame.origin.y, self.currentProfileLabel.frame.size.width, self.currentProfileLabel.frame.size.height);
|
|
||||||
self.currentProfileLabel.hidden = NO; // Show Current Profile display
|
|
||||||
if (@available(iOS 13.0, *)) {
|
|
||||||
self.currentProfileLabel.textAlignment = NSTextAlignmentNatural;
|
|
||||||
self.currentProfileLabel.numberOfLines = 2;
|
|
||||||
} else {
|
|
||||||
// Fallback on earlier versions
|
|
||||||
}
|
|
||||||
[self.currentProfileLabel setText:[LocalizationHelper localizedStringForKey:@"Profile: %@",[profilesManager getSelectedProfile].name]]; // display current profile name when profile is being refreshed.
|
|
||||||
|
|
||||||
// button size sliders
|
|
||||||
self.widgetSizeSlider.hidden = NO;
|
|
||||||
self.widgetSizeSlider.frame = CGRectMake(sliderXPosition, self.widgetSizeSlider.frame.origin.y, self.widgetSizeSlider.frame.size.width, self.widgetSizeSlider.frame.size.height);
|
|
||||||
[self.widgetSizeSlider addTarget:self action:@selector(widgetSizeSliderMoved) forControlEvents:(UIControlEventValueChanged)];
|
|
||||||
widgetSizeSliderLabel.text = [LocalizationHelper localizedStringForKey:@"Widget Size"];
|
|
||||||
widgetSizeSliderLabel.font = [UIFont systemFontOfSize:18];
|
|
||||||
widgetSizeSliderLabel.textColor = [UIColor whiteColor];
|
|
||||||
widgetSizeSliderLabel.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
|
|
||||||
widgetSizeSliderLabel.translatesAutoresizingMaskIntoConstraints = NO; // Disable autoresizing mask for Auto Layout
|
|
||||||
// Add slider and label to the view
|
|
||||||
[self.view addSubview:widgetSizeSliderLabel];
|
|
||||||
// Use Auto Layout to position the label relative to the slider
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
// Position the label to the left of the slider
|
|
||||||
[widgetSizeSliderLabel.trailingAnchor constraintEqualToAnchor:self.widgetSizeSlider.leadingAnchor constant:-10],
|
|
||||||
// Align vertically with the slider
|
|
||||||
[widgetSizeSliderLabel.centerYAnchor constraintEqualToAnchor:self.widgetSizeSlider.centerYAnchor]
|
|
||||||
]];
|
|
||||||
widgetSizeSliderLabel.hidden = NO;
|
|
||||||
|
|
||||||
// button height sliders
|
|
||||||
self.widgetHeightSlider.hidden = NO;
|
|
||||||
self.widgetHeightSlider.frame = CGRectMake(sliderXPosition, self.widgetHeightSlider.frame.origin.y, self.widgetHeightSlider.frame.size.width, self.widgetHeightSlider.frame.size.height);
|
|
||||||
[self.widgetHeightSlider addTarget:self action:@selector(widgetHeightSliderMoved) forControlEvents:(UIControlEventValueChanged)];
|
|
||||||
// button height label
|
|
||||||
widgetHeightSliderLabel.text = [LocalizationHelper localizedStringForKey:@"Widget Height"];
|
|
||||||
widgetHeightSliderLabel.font = [UIFont systemFontOfSize:18];
|
|
||||||
widgetHeightSliderLabel.textColor = [UIColor whiteColor];
|
|
||||||
widgetHeightSliderLabel.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
|
|
||||||
widgetHeightSliderLabel.translatesAutoresizingMaskIntoConstraints = NO; // Disable autoresizing mask for Auto Layout
|
|
||||||
// Add slider and label to the view
|
|
||||||
[self.view addSubview:widgetHeightSliderLabel];
|
|
||||||
// Use Auto Layout to position the label relative to the slider
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
// Position the label to the left of the slider
|
|
||||||
[widgetHeightSliderLabel.trailingAnchor constraintEqualToAnchor:self.widgetHeightSlider.leadingAnchor constant:-10],
|
|
||||||
// Align vertically with the slider
|
|
||||||
[widgetHeightSliderLabel.centerYAnchor constraintEqualToAnchor:self.widgetHeightSlider.centerYAnchor]
|
|
||||||
]];
|
|
||||||
widgetHeightSliderLabel.hidden = NO;
|
|
||||||
|
|
||||||
// button alpha label
|
|
||||||
self.widgetAlphaSlider.hidden = NO;
|
|
||||||
self.widgetAlphaSlider.frame = CGRectMake(sliderXPosition, self.widgetAlphaSlider.frame.origin.y, self.widgetAlphaSlider.frame.size.width, self.widgetAlphaSlider.frame.size.height);
|
|
||||||
[self.widgetAlphaSlider addTarget:self action:@selector(widgetAlphaSliderMoved) forControlEvents:(UIControlEventValueChanged)];
|
|
||||||
widgetAlphaSliderLabel.text = [LocalizationHelper localizedStringForKey:@"Widget Alpha"];
|
|
||||||
widgetAlphaSliderLabel.font = [UIFont systemFontOfSize:18];
|
|
||||||
widgetAlphaSliderLabel.textColor = [UIColor whiteColor];
|
|
||||||
widgetAlphaSliderLabel.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
|
|
||||||
widgetAlphaSliderLabel.translatesAutoresizingMaskIntoConstraints = NO; // Disable autoresizing mask for Auto Layout
|
|
||||||
// Add slider and label to the view
|
|
||||||
[self.view addSubview:widgetAlphaSliderLabel];
|
|
||||||
// Use Auto Layout to position the label relative to the slider
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
// Position the label to the left of the slider
|
|
||||||
[widgetAlphaSliderLabel.trailingAnchor constraintEqualToAnchor:self.widgetAlphaSlider.leadingAnchor constant:-10],
|
|
||||||
// Align vertically with the slider
|
|
||||||
[widgetAlphaSliderLabel.centerYAnchor constraintEqualToAnchor:self.widgetAlphaSlider.centerYAnchor]
|
|
||||||
]];
|
|
||||||
widgetAlphaSliderLabel.hidden = NO;
|
|
||||||
|
|
||||||
|
|
||||||
// border Width slider
|
|
||||||
self.widgetBorderWidthSlider.hidden = NO;
|
|
||||||
self.widgetBorderWidthSlider.frame = CGRectMake(CGRectGetMaxX(self.currentProfileLabel.frame)-self.widgetBorderWidthSlider.frame.size.width, self.widgetBorderWidthSlider.frame.origin.y, self.widgetBorderWidthSlider.frame.size.width, self.widgetBorderWidthSlider.frame.size.height);
|
|
||||||
|
|
||||||
[self.widgetBorderWidthSlider addTarget:self action:@selector(widgetBorderWidthSliderMoved) forControlEvents:(UIControlEventValueChanged)];
|
|
||||||
widgetBorderWidthSliderLabel.text = [LocalizationHelper localizedStringForKey:@"Border Width"];
|
|
||||||
widgetBorderWidthSliderLabel.font = [UIFont systemFontOfSize:18];
|
|
||||||
widgetBorderWidthSliderLabel.textColor = [UIColor whiteColor];
|
|
||||||
widgetBorderWidthSliderLabel.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
|
|
||||||
widgetBorderWidthSliderLabel.translatesAutoresizingMaskIntoConstraints = NO; // Disable autoresizing mask for Auto Layout
|
|
||||||
[self.view addSubview:widgetBorderWidthSliderLabel];
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
// Position the label to the left of the slider
|
|
||||||
[widgetBorderWidthSliderLabel.trailingAnchor constraintEqualToAnchor:self.widgetBorderWidthSlider.leadingAnchor constant:-10],
|
|
||||||
// Align vertically with the slider
|
|
||||||
[widgetBorderWidthSliderLabel.centerYAnchor constraintEqualToAnchor:self.widgetBorderWidthSlider.centerYAnchor]
|
|
||||||
]];
|
|
||||||
widgetBorderWidthSliderLabel.hidden = NO;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// sensitivity slider
|
|
||||||
self.sensitivityFactorSlider.hidden = YES;
|
|
||||||
self.sensitivityFactorSlider.frame = CGRectMake(sliderXPosition, self.sensitivityFactorSlider.frame.origin.y, self.sensitivityFactorSlider.frame.size.width, self.sensitivityFactorSlider.frame.size.height);
|
|
||||||
[self.sensitivityFactorSlider addTarget:self action:@selector(sensitivitySliderMoved) forControlEvents:(UIControlEventValueChanged)];
|
|
||||||
sensitivitySliderLabel.text = [LocalizationHelper localizedStringForKey:@"Sensitivity"];
|
|
||||||
sensitivitySliderLabel.font = [UIFont systemFontOfSize:18];
|
|
||||||
sensitivitySliderLabel.textColor = [UIColor whiteColor];
|
|
||||||
sensitivitySliderLabel.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
|
|
||||||
sensitivitySliderLabel.translatesAutoresizingMaskIntoConstraints = NO; // Disable autoresizing mask for Auto Layout
|
|
||||||
// Add slider and label to the view
|
|
||||||
[self.view addSubview:sensitivitySliderLabel];
|
|
||||||
// Use Auto Layout to position the label relative to the slider
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
// Position the label to the left of the slider
|
|
||||||
[sensitivitySliderLabel.trailingAnchor constraintEqualToAnchor:self.sensitivityFactorSlider.leadingAnchor constant:-10],
|
|
||||||
// Align vertically with the slider
|
|
||||||
[sensitivitySliderLabel.centerYAnchor constraintEqualToAnchor:self.sensitivityFactorSlider.centerYAnchor]
|
|
||||||
]];
|
|
||||||
sensitivitySliderLabel.hidden = self.sensitivityFactorSlider.hidden;
|
|
||||||
|
|
||||||
|
|
||||||
// stick indicator offset slider
|
|
||||||
self.stickIndicatorOffsetSlider.hidden = YES;
|
|
||||||
self.stickIndicatorOffsetSlider.frame = CGRectMake(sliderXPosition, self.stickIndicatorOffsetSlider.frame.origin.y, self.stickIndicatorOffsetSlider.frame.size.width, self.stickIndicatorOffsetSlider.frame.size.height); // make this label bigger to show some tips
|
|
||||||
[self.stickIndicatorOffsetSlider addTarget:self action:@selector(stickIndicatorOffsetSliderMoved) forControlEvents:(UIControlEventValueChanged)];
|
|
||||||
stickIndicatorOffsetSliderLabel.text = [LocalizationHelper localizedStringForKey:@"Indicator Offset"];
|
|
||||||
stickIndicatorOffsetSliderLabel.font = [UIFont systemFontOfSize:18];
|
|
||||||
stickIndicatorOffsetSliderLabel.textColor = [UIColor whiteColor];
|
|
||||||
stickIndicatorOffsetSliderLabel.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
|
|
||||||
stickIndicatorOffsetSliderLabel.translatesAutoresizingMaskIntoConstraints = NO; // Disable autoresizing mask for Auto Layout
|
|
||||||
// tips for stick indicator
|
|
||||||
stickIndicatorOffsetExplain.text = [LocalizationHelper localizedStringForKey:@"Ball: assumed touch point. Cross: indicator location"];
|
|
||||||
//stickIndicatorOffsetExplain.frame = CGRectMake(sliderXPosition, self.stickIndicatorOffsetSlider.frame.origin.y + self.stickIndicatorOffsetSlider.frame.size.height, self.stickIndicatorOffsetSlider.frame.size.width, self.stickIndicatorOffsetSlider.frame.size.height); // label bigger show some tips
|
|
||||||
stickIndicatorOffsetExplain.font = [UIFont systemFontOfSize:18];
|
|
||||||
stickIndicatorOffsetExplain.textColor = [UIColor whiteColor];
|
|
||||||
stickIndicatorOffsetExplain.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
|
|
||||||
stickIndicatorOffsetExplain.translatesAutoresizingMaskIntoConstraints = NO; // Disable autoresizing mask for Auto Layout
|
|
||||||
// Add slider and label to the view
|
|
||||||
[self.view addSubview:stickIndicatorOffsetSliderLabel];
|
|
||||||
[self.view addSubview:stickIndicatorOffsetExplain];
|
|
||||||
// Use Auto Layout to position the label relative to the slider
|
|
||||||
[NSLayoutConstraint activateConstraints:@[
|
|
||||||
// Position the label to the left of the slider
|
|
||||||
[stickIndicatorOffsetSliderLabel.trailingAnchor constraintEqualToAnchor:self.stickIndicatorOffsetSlider.leadingAnchor constant:-10],
|
|
||||||
// Align vertically with the slider
|
|
||||||
[stickIndicatorOffsetSliderLabel.centerYAnchor constraintEqualToAnchor:self.stickIndicatorOffsetSlider.centerYAnchor],
|
|
||||||
// Position the explain label
|
|
||||||
[stickIndicatorOffsetExplain.centerXAnchor constraintEqualToAnchor:self.stickIndicatorOffsetSlider.leadingAnchor constant:-10],
|
|
||||||
[stickIndicatorOffsetExplain.topAnchor constraintEqualToAnchor:self.stickIndicatorOffsetSlider.bottomAnchor]
|
|
||||||
]];
|
|
||||||
stickIndicatorOffsetSliderLabel.hidden = stickIndicatorOffsetExplain.hidden = self.stickIndicatorOffsetSlider.hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Basically the same method as loadTapped, without parameter*/
|
|
||||||
// Make sure whenever self view controller load the selected profile and layout its buttons.
|
|
||||||
- (void)profileRefresh{
|
|
||||||
UIStoryboard *storyboard;
|
|
||||||
BOOL isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
|
||||||
if (isIPhone) {
|
|
||||||
storyboard = [UIStoryboard storyboardWithName:@"iPhone" bundle:nil];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
storyboard = [UIStoryboard storyboardWithName:@"iPad" bundle:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup: current profile lable, button width slider, button height slider & button alpha slider
|
|
||||||
[self setupProfileLableAndSliders];
|
|
||||||
|
|
||||||
//initialiaze _oscProfilesTableViewController
|
|
||||||
self->_oscProfilesTableViewController = [storyboard instantiateViewControllerWithIdentifier:@"OSCProfilesTableViewController"];
|
|
||||||
|
|
||||||
//this part is just for registration, will not be immediately executed.
|
|
||||||
self->_oscProfilesTableViewController.needToUpdateOscLayoutTVC = ^() { // a block that will be called when the modally presented 'OSCProfilesTableViewController' VC is dismissed. By the time the 'OSCProfilesTableViewController' VC is dismissed the user would have potentially selected a different OSC profile with a different layout and they want to see this layout on this 'LayoutOnScreenControlsViewController.' This block of code will load the profile and then hide/show and move each OSC button to their appropriate position
|
|
||||||
[self.layoutOSC updateControls]; // creates and saves a 'Default' OSC profile or loads the one the user selected on the previous screen
|
|
||||||
[self addInnerAnalogSticksToOuterAnalogLayers];
|
|
||||||
[self.layoutOSC.layoutChanges removeAllObjects]; // since a new OSC profile is being loaded, this will remove all previous layout changes made from the array
|
|
||||||
[self OSCLayoutChanged]; // fades the 'Undo Button' out
|
|
||||||
self->_oscProfilesTableViewController.currentOSCButtonLayers = self.layoutOSC.OSCButtonLayers; //pass updated OSCLayout to OSCProfileTableView again
|
|
||||||
//[self reloadOnScreenKeyboardButtons];
|
|
||||||
};
|
|
||||||
|
|
||||||
[self.oscProfilesTableViewController profileViewRefresh]; // execute this will make sure OSCLayout is updated from persisted profile, not any cache.
|
|
||||||
[self reloadOnScreenWidgetViews];
|
|
||||||
|
|
||||||
// [self presentViewController:vc animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Presents the view controller that lists all OSC profiles the user can choose from */
|
|
||||||
- (IBAction) loadTapped:(id)sender {
|
|
||||||
UIStoryboard *storyboard;
|
|
||||||
BOOL isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
|
||||||
if (isIPhone) {
|
|
||||||
storyboard = [UIStoryboard storyboardWithName:@"iPhone" bundle:nil];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
storyboard = [UIStoryboard storyboardWithName:@"iPad" bundle:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
_oscProfilesTableViewController = [storyboard instantiateViewControllerWithIdentifier:@"OSCProfilesTableViewController"] ;
|
|
||||||
|
|
||||||
_oscProfilesTableViewController.needToUpdateOscLayoutTVC = ^() { // a block that will be called when the modally presented 'OSCProfilesTableViewController' VC is dismissed. By the time the 'OSCProfilesTableViewController' VC is dismissed the user would have potentially selected a different OSC ofile with a different layout and they want to see this layout on this 'LayoutOnScreenControlsViewController.' This block of code will load the profile and then hide/show and move each OSC button to their appropriate position
|
|
||||||
[self.layoutOSC updateControls]; // creates and saves a 'Default' OSC profile or loads the one the user selected on the previous screen
|
|
||||||
|
|
||||||
[self addInnerAnalogSticksToOuterAnalogLayers];
|
|
||||||
|
|
||||||
[self.layoutOSC.layoutChanges removeAllObjects]; // since a new OSC profile is being loaded, this will remove all previous layout changes made from the array
|
|
||||||
|
|
||||||
[self OSCLayoutChanged]; // fades the 'Undo Button' out
|
|
||||||
};
|
|
||||||
self.currentProfileLabel.hidden = YES; // Hide Current Profile display before entering the profile table view
|
|
||||||
self.widgetSizeSlider.hidden = YES;
|
|
||||||
self.widgetHeightSlider.hidden = YES;
|
|
||||||
self.widgetAlphaSlider.hidden = YES;
|
|
||||||
widgetSizeSliderLabel.hidden = YES;
|
|
||||||
widgetHeightSliderLabel.hidden = YES;
|
|
||||||
widgetAlphaSliderLabel.hidden = YES;
|
|
||||||
_oscProfilesTableViewController.currentOSCButtonLayers = self.layoutOSC.OSCButtonLayers;
|
|
||||||
[self presentViewController:_oscProfilesTableViewController animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Touch
|
|
||||||
|
|
||||||
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
for (UITouch* touch in touches) {
|
|
||||||
|
|
||||||
CGPoint touchLocation = [touch locationInView:self.view];
|
|
||||||
touchLocation = [[touch view] convertPoint:touchLocation toView:nil];
|
|
||||||
CALayer *layer = [self.view.layer hitTest:touchLocation];
|
|
||||||
|
|
||||||
if (layer == self.toolbarRootView.layer ||
|
|
||||||
layer == self.chevronView.layer ||
|
|
||||||
layer == self.chevronImageView.layer ||
|
|
||||||
layer == self.toolbarStackView.layer ||
|
|
||||||
layer == self.view.layer) { // don't let user move toolbar or toolbar UI buttons, toolbar's chevron 'pull tab', or the layer associated with this VC's view
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[self.layoutOSC touchesBegan:touches withEvent:event];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
||||||
// for OnScreenWidgets:
|
|
||||||
|
|
||||||
|
|
||||||
// -------- for OSC buttons
|
|
||||||
[self.layoutOSC touchesMoved:touches withEvent:event];
|
|
||||||
if ([self.layoutOSC isLayer:self.layoutOSC.layerBeingDragged
|
|
||||||
hoveringOverButton:trashCanButton]) { // check if user is dragging around a button and hovering it over the trash can button
|
|
||||||
trashCanButton.tintColor = [UIColor redColor];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
trashCanButton.tintColor = [UIColor colorWithRed:171.0/255.0 green:157.0/255.0 blue:255.0/255.0 alpha:1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------- for keyboard Buttons
|
|
||||||
UITouch *touch = [touches anyObject]; // Get the first touch in the set
|
|
||||||
if([self touchWithinTashcanButton:touch]){
|
|
||||||
trashCanButton.tintColor = [UIColor redColor];
|
|
||||||
}
|
|
||||||
else trashCanButton.tintColor = [UIColor colorWithRed:171.0/255.0 green:157.0/255.0 blue:255.0/255.0 alpha:1];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (bool)touchWithinTashcanButton:(UITouch* )touch {
|
|
||||||
CGPoint locationInView = [touch locationInView:self.view];
|
|
||||||
|
|
||||||
// Convert the location to the button's coordinate system
|
|
||||||
CGPoint locationInButton = [self.view convertPoint:locationInView toView:trashCanButton];
|
|
||||||
bool ret = CGRectContainsPoint(trashCanButton.bounds, locationInButton);
|
|
||||||
// NSLog(@"within button: %d", ret);
|
|
||||||
// Check if the location is within the button's bounds
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
||||||
|
|
||||||
// removing keyboard buttons objs
|
|
||||||
UITouch *touch = [touches anyObject]; // Get the first touch in the set
|
|
||||||
if([self touchWithinTashcanButton:touch]){
|
|
||||||
if(self->selectedWidgetView != nil){
|
|
||||||
[self->selectedWidgetView removeFromSuperview];
|
|
||||||
[self.OnScreenWidgetViews removeObject:self->selectedWidgetView];
|
|
||||||
[selectedWidgetView.stickBallLayer removeFromSuperlayer];
|
|
||||||
[selectedWidgetView.crossMarkLayer removeFromSuperlayer];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//removing OSC buttons
|
|
||||||
if (self.layoutOSC.layerBeingDragged != nil &&
|
|
||||||
[self.layoutOSC isLayer:self.layoutOSC.layerBeingDragged hoveringOverButton:trashCanButton]) { // check if user wants to throw OSC button into the trash can
|
|
||||||
// here we're going to delete something
|
|
||||||
|
|
||||||
self.layoutOSC.layerBeingDragged.hidden = YES;
|
|
||||||
|
|
||||||
if ([self.layoutOSC.layerBeingDragged.name isEqualToString:@"dPad"]) { // if user is hiding dPad, then hide all four dPad button child layers as well since setting the 'hidden' property on the parent dPad CALayer doesn't automatically hide the four child CALayer dPad buttons
|
|
||||||
self.layoutOSC._upButton.hidden = YES;
|
|
||||||
self.layoutOSC._rightButton.hidden = YES;
|
|
||||||
self.layoutOSC._downButton.hidden = YES;
|
|
||||||
self.layoutOSC._leftButton.hidden = YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* if user is hiding left or right analog sticks, then hide their corresponding inner analog stick child layers as well since setting the 'hidden' property on the parent analog stick doesn't automatically hide its child inner analog stick CALayer */
|
|
||||||
if ([self.layoutOSC.layerBeingDragged.name isEqualToString:@"leftStickBackground"]) {
|
|
||||||
self.layoutOSC._leftStick.hidden = YES;
|
|
||||||
}
|
|
||||||
if ([self.layoutOSC.layerBeingDragged.name isEqualToString:@"rightStickBackground"]) {
|
|
||||||
self.layoutOSC._rightStick.hidden = YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[self.layoutOSC touchesEnded:touches withEvent:event];
|
|
||||||
|
|
||||||
// in case of default profile OSC change, popup msgbox & remind user it's not allowed.
|
|
||||||
if([profilesManager getIndexOfSelectedProfile] == 0 && [self.layoutOSC.layoutChanges count] > 0){
|
|
||||||
UIAlertController * movedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Layout of the Default profile can not be changed"] preferredStyle:UIAlertControllerStyleAlert];
|
|
||||||
[movedAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
|
||||||
[self.oscProfilesTableViewController profileViewRefresh];
|
|
||||||
}]];
|
|
||||||
[self presentViewController:movedAlertController animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
trashCanButton.tintColor = [UIColor colorWithRed:171.0/255.0 green:157.0/255.0 blue:255.0/255.0 alpha:1];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
//
|
|
||||||
// OSCProfilesTableViewController.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Long Le on 11/28/22.
|
|
||||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
/**
|
|
||||||
This view displays a list of on screen controller profiles and gives the user the ability to select any of the profiles to be the 'Selected' profile whose on screen controller layout configuration will be shown on the game stream view, or in the on screen controller layout view. This view also allows the user to swipe and delete any of the listed profiles.
|
|
||||||
*/
|
|
||||||
@interface OSCProfilesTableViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
|
|
||||||
|
|
||||||
- (void)profileViewRefresh;
|
|
||||||
|
|
||||||
|
|
||||||
@property (weak, nonatomic) IBOutlet UITableView *tableView;
|
|
||||||
@property (nonatomic, copy) void (^needToUpdateOscLayoutTVC)(void);
|
|
||||||
@property (nonatomic, assign) NSMutableArray *currentOSCButtonLayers;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
//
|
|
||||||
// OSCProfilesTableViewController.m
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Long Le on 11/28/22.
|
|
||||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "OSCProfilesTableViewController.h"
|
|
||||||
#import "LayoutOnScreenControlsViewController.h"
|
|
||||||
#import "ProfileTableViewCell.h"
|
|
||||||
#import "OSCProfile.h"
|
|
||||||
#import "OnScreenButtonState.h"
|
|
||||||
#import "OSCProfilesManager.h"
|
|
||||||
#import "LocalizationHelper.h"
|
|
||||||
|
|
||||||
const double NAV_BAR_HEIGHT = 50;
|
|
||||||
|
|
||||||
@interface OSCProfilesTableViewController ()
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation OSCProfilesTableViewController {
|
|
||||||
OSCProfilesManager *profilesManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@synthesize tableView;
|
|
||||||
|
|
||||||
- (UIInterfaceOrientationMask)getCurrentOrientation{
|
|
||||||
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]);
|
|
||||||
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
|
|
||||||
//lock the orientation accordingly after streaming is started
|
|
||||||
if(screenWidthInPoints > screenHeightInPoints) return UIInterfaceOrientationMaskLandscape;
|
|
||||||
else return UIInterfaceOrientationMaskPortrait|UIInterfaceOrientationMaskPortraitUpsideDown;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
|
|
||||||
// Return the supported interface orientations acoordingly
|
|
||||||
return [self getCurrentOrientation]; // 90 Degree rotation not allowed in streaming or app view
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) viewWillDisappear:(BOOL)animated{
|
|
||||||
[super viewWillDisappear:animated];
|
|
||||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"OscLayoutTableViewCloseNotification" object:self]; // notify other view that oscLayoutManager is closing
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void) viewDidLoad {
|
|
||||||
[super viewDidLoad];
|
|
||||||
|
|
||||||
|
|
||||||
profilesManager = [OSCProfilesManager sharedManager];
|
|
||||||
|
|
||||||
self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, NAV_BAR_HEIGHT)];
|
|
||||||
|
|
||||||
self.tableView.delegate = self;
|
|
||||||
self.tableView.dataSource = self;
|
|
||||||
|
|
||||||
[self.tableView registerNib:[UINib nibWithNibName:@"ProfileTableViewCell" bundle:nil]
|
|
||||||
forCellReuseIdentifier:@"Cell"]; // Register the custom cell nib file with the table view
|
|
||||||
self.tableView.alpha = 0.5;
|
|
||||||
self.tableView.backgroundColor = [[UIColor colorWithRed:0.5 green:0.7 blue:1.0 alpha:1.0] colorWithAlphaComponent:0.2]; // set background color & transparency
|
|
||||||
// self.tableView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.2]; // set background color & transparency
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) viewDidAppear:(BOOL)animated {
|
|
||||||
[super viewDidAppear: animated];
|
|
||||||
|
|
||||||
self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, NAV_BAR_HEIGHT)];
|
|
||||||
|
|
||||||
self.tableView.delegate = self;
|
|
||||||
self.tableView.dataSource = self;
|
|
||||||
|
|
||||||
[self.tableView registerNib:[UINib nibWithNibName:@"ProfileTableViewCell" bundle:nil]
|
|
||||||
forCellReuseIdentifier:@"Cell"]; // Register the custom cell nib file with the table view
|
|
||||||
|
|
||||||
if ([[profilesManager getAllProfiles] count] > 0) { // scroll to selected profile if user has any saved profiles
|
|
||||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[profilesManager getIndexOfSelectedProfile] inSection:0];
|
|
||||||
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - UIButton Actions
|
|
||||||
|
|
||||||
/* Loads the OSC profile that user selected, dismisses this VC, then tells the presenting view controller to lay out the on screen buttons according to the selected profile's instructions */
|
|
||||||
- (IBAction) duplicateTapped:(id)sender {
|
|
||||||
|
|
||||||
UIAlertController * inputNameAlertController = [UIAlertController alertControllerWithTitle: [LocalizationHelper localizedStringForKey:@"Enter the name you want to save this profile as"] message: @"" preferredStyle:UIAlertControllerStyleAlert];
|
|
||||||
[inputNameAlertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { // pop up notification with text field where user can enter the text they wish to name their OSC layout profile
|
|
||||||
textField.placeholder = [LocalizationHelper localizedStringForKey:@"name"];
|
|
||||||
textField.textColor = [UIColor lightGrayColor];
|
|
||||||
textField.clearButtonMode = UITextFieldViewModeWhileEditing;
|
|
||||||
textField.borderStyle = UITextBorderStyleNone;
|
|
||||||
}];
|
|
||||||
[inputNameAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Cancel"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { // adds a button that allows user to decline the option to save the controller layout they currently see on screen
|
|
||||||
[inputNameAlertController dismissViewControllerAnimated:NO completion:nil];
|
|
||||||
}]];
|
|
||||||
[inputNameAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Save"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { // add save button to allow user to save the on screen controller configuration
|
|
||||||
NSArray *textFields = inputNameAlertController.textFields;
|
|
||||||
UITextField *nameField = textFields[0];
|
|
||||||
NSString *enteredProfileName = nameField.text;
|
|
||||||
|
|
||||||
if ([enteredProfileName isEqualToString:@"Default"]) { // don't let user user overwrite the 'Default' profile
|
|
||||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Saving over the 'Default' profile is not allowed"] preferredStyle:UIAlertControllerStyleAlert];
|
|
||||||
|
|
||||||
[alertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
|
||||||
[alertController dismissViewControllerAnimated:NO completion:^{
|
|
||||||
[self presentViewController:inputNameAlertController animated:YES completion:nil];
|
|
||||||
}];
|
|
||||||
}]];
|
|
||||||
[self presentViewController:alertController animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
else if ([enteredProfileName length] == 0) { // if user entered no text and taps the 'Save' button let them know they can't do that
|
|
||||||
UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Profile name cannot be blank!"] preferredStyle:UIAlertControllerStyleAlert];
|
|
||||||
|
|
||||||
[savedAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { // show pop up notification letting user know they must enter a name in the text field if they wish to save the controller profile
|
|
||||||
|
|
||||||
[savedAlertController dismissViewControllerAnimated:NO completion:^{
|
|
||||||
[self presentViewController:inputNameAlertController animated:YES completion:nil];
|
|
||||||
}];
|
|
||||||
}]];
|
|
||||||
[self presentViewController:savedAlertController animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
else if ([self->profilesManager profileNameAlreadyExist:enteredProfileName] == YES) { // if the entered profile name already
|
|
||||||
UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Profile name already exists"] preferredStyle:UIAlertControllerStyleAlert];
|
|
||||||
|
|
||||||
[savedAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
|
||||||
//[savedAlertController dismissViewControllerAnimated:NO completion:nil];
|
|
||||||
//[self profileViewRefresh]; //refresh profile view after saving new profile;
|
|
||||||
}]];
|
|
||||||
[self presentViewController:savedAlertController animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
else { // if user entered a valid name that doesn't already exist then save the profile to persistent storage
|
|
||||||
[self->profilesManager saveProfileWithName: enteredProfileName andButtonLayers:self.currentOSCButtonLayers]; // the OSC layout here is passed from parent LayoutOSCViewController;
|
|
||||||
[self->profilesManager setProfileToSelected: enteredProfileName];
|
|
||||||
|
|
||||||
UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Profile %@ duplicated from current layout", enteredProfileName] preferredStyle:UIAlertControllerStyleAlert]; // Let user know this profile has been duplicated & saved
|
|
||||||
|
|
||||||
[savedAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
|
||||||
[savedAlertController dismissViewControllerAnimated:NO completion:nil];
|
|
||||||
[self profileViewRefresh]; //refresh profile view after saving new profile;
|
|
||||||
}]];
|
|
||||||
[self presentViewController:savedAlertController animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
}]];
|
|
||||||
|
|
||||||
[self presentViewController:inputNameAlertController animated:YES completion:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* basically the same with loadTapped */
|
|
||||||
- (void) profileViewRefresh{
|
|
||||||
//[self dismissViewControllerAnimated:YES completion:nil];
|
|
||||||
//[selfparentLayoutOSCViewController]
|
|
||||||
[self.tableView reloadData]; // table view will be refreshed by calling reloadData
|
|
||||||
|
|
||||||
if (self.needToUpdateOscLayoutTVC) { // tells the presenting view controller to lay out the on screen buttons according to the selected profile's instructions
|
|
||||||
self.needToUpdateOscLayoutTVC();
|
|
||||||
}
|
|
||||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"OscLayoutProfileSelctedInTableView" object:self]; // notify other view that oscLayoutManager is closing
|
|
||||||
}
|
|
||||||
|
|
||||||
- (IBAction) deleteTapped:(id)sender{// delete can be executed simply by calling this 2 methods.
|
|
||||||
[profilesManager deleteCurrentSelectedProfile];
|
|
||||||
[self profileViewRefresh];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (IBAction) exitTapped:(id)sender {
|
|
||||||
[self dismissViewControllerAnimated:YES completion:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - TableView DataSource
|
|
||||||
|
|
||||||
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
|
||||||
return [[profilesManager getAllProfiles] count];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
||||||
ProfileTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
|
|
||||||
OSCProfile *profile = [[profilesManager getAllProfiles] objectAtIndex:indexPath.row];
|
|
||||||
cell.name.text = profile.name;
|
|
||||||
cell.name.backgroundColor = [UIColor clearColor];
|
|
||||||
cell.name.alpha = 1.0;
|
|
||||||
cell.name.textColor = [UIColor whiteColor]; // Opaque white text
|
|
||||||
cell.name.shadowColor = [UIColor blackColor]; // Black shadow
|
|
||||||
// Set the shadow offset
|
|
||||||
cell.name.shadowOffset = CGSizeMake(1.0, 1.5);
|
|
||||||
|
|
||||||
// Set cell and contentView background colors
|
|
||||||
cell.backgroundColor = [UIColor clearColor];
|
|
||||||
cell.contentView.backgroundColor = [UIColor clearColor];
|
|
||||||
|
|
||||||
// Configure the checkmark accessory
|
|
||||||
if ([profile.name isEqualToString:[profilesManager getSelectedProfile].name]) {
|
|
||||||
cell.accessoryType = UITableViewCellAccessoryCheckmark;
|
|
||||||
} else {
|
|
||||||
cell.accessoryType = UITableViewCellAccessoryNone;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove existing custom separators to avoid duplicates
|
|
||||||
UIView *existingSeparator = [cell.contentView viewWithTag:100];
|
|
||||||
if (existingSeparator) {
|
|
||||||
[existingSeparator removeFromSuperview];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add custom separator with increased height (thickness)
|
|
||||||
CGFloat separatorHeight = 1.0; // Adjust this value for thicker line
|
|
||||||
UIView *separatorView = [[UIView alloc] initWithFrame:CGRectMake(0, cell.contentView.frame.size.height - separatorHeight, cell.contentView.frame.size.width, separatorHeight)];
|
|
||||||
|
|
||||||
separatorView.backgroundColor = [UIColor whiteColor]; // Set your desired color
|
|
||||||
separatorView.tag = 100; // Assign a tag to identify the separator view
|
|
||||||
separatorView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
|
|
||||||
|
|
||||||
[cell.contentView addSubview:separatorView];
|
|
||||||
[cell.contentView bringSubviewToFront:separatorView];
|
|
||||||
|
|
||||||
|
|
||||||
// Replace the default checkmark with a UILabel displaying a checkmark character
|
|
||||||
if ([profile.name isEqualToString:[profilesManager getSelectedProfile].name]) {
|
|
||||||
UILabel *checkmarkLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; // Adjust size as needed
|
|
||||||
checkmarkLabel.text = @"✓"; // The checkmark character
|
|
||||||
checkmarkLabel.font = [UIFont systemFontOfSize:25]; // Adjust font size as needed
|
|
||||||
checkmarkLabel.textAlignment = NSTextAlignmentCenter;
|
|
||||||
checkmarkLabel.textColor = [UIColor greenColor]; // Set the color of the checkmark
|
|
||||||
cell.accessoryView = checkmarkLabel;
|
|
||||||
checkmarkLabel.layer.zPosition = 0; // Push checkmark to the back
|
|
||||||
} else {
|
|
||||||
cell.accessoryView = nil; // Remove checkmark if not selected
|
|
||||||
}
|
|
||||||
[cell.contentView bringSubviewToFront:separatorView];
|
|
||||||
separatorView.layer.zPosition = 1; // Bring separator to the top layer
|
|
||||||
|
|
||||||
|
|
||||||
return cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL) tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
||||||
return 60.0; // Set your desired cell height here
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void) tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
||||||
NSMutableArray *profiles = [profilesManager getAllProfiles];
|
|
||||||
|
|
||||||
if ([[[profiles objectAtIndex:indexPath.row] name] isEqualToString:@"Default"]) { // if user is attempting to delete the 'Default' profile then show a pop up telling user they can't do that and return out of this method
|
|
||||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: @"Deleting the 'Default' profile is not allowed" preferredStyle:UIAlertControllerStyleAlert];
|
|
||||||
|
|
||||||
[alertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
|
||||||
[alertController dismissViewControllerAnimated:NO completion:nil];
|
|
||||||
}]];
|
|
||||||
[self presentViewController:alertController animated:YES completion:nil];
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editingStyle == UITableViewCellEditingStyleDelete) {
|
|
||||||
OSCProfile *profile = [profiles objectAtIndex:indexPath.row];
|
|
||||||
if (profile.isSelected) { // if user is deleting the currently selected OSC profile then make the profile at its previous index the currently selected profile
|
|
||||||
if (indexPath.row > 0) { // check that row is greater than zero to avoid an out of bounds crash, although that should not be possible right now since the 'Default' profile is always at row 0 and they're not allowed to delete it
|
|
||||||
OSCProfile *profile = [profiles objectAtIndex:indexPath.row - 1];
|
|
||||||
profile.isSelected = YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[profiles removeObjectAtIndex:indexPath.row];
|
|
||||||
|
|
||||||
/* save OSC profiles array to persistent storage */
|
|
||||||
NSMutableArray *profilesEncoded = [[NSMutableArray alloc] init];
|
|
||||||
for (OSCProfile *profileDecoded in profiles) { // encode each OSC profile object and add them to an array
|
|
||||||
|
|
||||||
NSData *profileEncoded = [NSKeyedArchiver archivedDataWithRootObject:profileDecoded requiringSecureCoding:YES error:nil];
|
|
||||||
[profilesEncoded addObject:profileEncoded];
|
|
||||||
}
|
|
||||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded
|
|
||||||
requiringSecureCoding:YES error:nil]; // encode the array itself, NOT the objects in the array
|
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
|
||||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
||||||
|
|
||||||
[tableView reloadData];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - TableView Delegate
|
|
||||||
|
|
||||||
/* When user taps a cell it moves the checkmark to that cell indicating to the user the profile associated with that cell is now the selected profile. It also sets that cell's associated OSCProfile object's 'isSelected' property to YES */
|
|
||||||
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
|
||||||
{
|
|
||||||
NSIndexPath *selectedIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:0];
|
|
||||||
NSIndexPath *lastSelectedIndexPath = [NSIndexPath indexPathForRow:[profilesManager getIndexOfSelectedProfile] inSection:0];
|
|
||||||
|
|
||||||
if (selectedIndexPath != lastSelectedIndexPath) {
|
|
||||||
/* Place checkmark on selected cell and set profile associated with cell as selected profile */
|
|
||||||
UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath: selectedIndexPath];
|
|
||||||
selectedCell.accessoryType = UITableViewCellAccessoryCheckmark; // add checkmark to the cell the user tapped
|
|
||||||
selectedCell.accessoryView.tintColor = [[UIColor colorWithRed:0.5 green:0.5 blue:1.0 alpha:0.85] colorWithAlphaComponent:1.0];
|
|
||||||
OSCProfile *profile = [[profilesManager getAllProfiles] objectAtIndex:indexPath.row];
|
|
||||||
[profilesManager setProfileToSelected: profile.name]; // set the profile associated with this cell's 'isSelected' property to YES
|
|
||||||
|
|
||||||
/* Remove checkmark on the previously selected cell */
|
|
||||||
UITableViewCell *lastSelectedCell = [tableView cellForRowAtIndexPath: lastSelectedIndexPath];
|
|
||||||
lastSelectedCell.accessoryType = UITableViewCellAccessoryNone;
|
|
||||||
[tableView deselectRowAtIndexPath:lastSelectedIndexPath animated:YES];
|
|
||||||
}
|
|
||||||
[self profileViewRefresh]; // update OSC layout when table view option is changed
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
//
|
|
||||||
// MainFrameViewController.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 1/17/14.
|
|
||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
#import "DiscoveryManager.h"
|
|
||||||
#import "PairManager.h"
|
|
||||||
#import "StreamConfiguration.h"
|
|
||||||
#import "UIComputerView.h"
|
|
||||||
#import "UIAppView.h"
|
|
||||||
#import "AppAssetManager.h"
|
|
||||||
#import "SWRevealViewController.h"
|
|
||||||
|
|
||||||
@interface MainFrameViewController : UICollectionViewController <DiscoveryCallback, PairCallback, HostCallback, AppCallback, AppAssetCallback, NSURLConnectionDelegate, SWRevealViewControllerDelegate>
|
|
||||||
|
|
||||||
@property (weak, nonatomic) IBOutlet UIBarButtonItem *settingsButton;
|
|
||||||
#if !TARGET_OS_TV
|
|
||||||
@property (weak, nonatomic) IBOutlet UIBarButtonItem *upButton;
|
|
||||||
@property (nonatomic, assign) bool settingsExpandedInStreamView;
|
|
||||||
|
|
||||||
-(void)simulateSettingsButtonPress;
|
|
||||||
-(void)reloadStreamConfig;
|
|
||||||
-(bool)isIPhonePortrait;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *touchModeSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *touchModeSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UILabel *onscreenControllerLabel;
|
@property (strong, nonatomic) IBOutlet UILabel *onscreenControllerLabel;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *onscreenControlSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *onscreenControlSelector;
|
||||||
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *motionModeSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *largerStickLR1Selector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *largerStickLR1Selector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *optimizeSettingsSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *optimizeSettingsSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *multiControllerSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *multiControllerSelector;
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *audioOnPCSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *audioOnPCSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *codecSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *codecSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *yuv444Selector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *yuv444Selector;
|
||||||
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *pipSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *hdrSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *hdrSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector;
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)updateResolutionTable{
|
- (void)updateResolutionTable{
|
||||||
UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
|
UIWindow *window = self.view.window;
|
||||||
CGFloat screenScale = window.screen.scale;
|
CGFloat screenScale = window.screen.scale;
|
||||||
CGFloat safeAreaWidth = (window.frame.size.width - window.safeAreaInsets.left - window.safeAreaInsets.right) * screenScale;
|
CGFloat safeAreaWidth = (window.frame.size.width - window.safeAreaInsets.left - window.safeAreaInsets.right) * screenScale;
|
||||||
CGFloat appWindowWidth = window.frame.size.width * screenScale;
|
CGFloat appWindowWidth = window.frame.size.width * screenScale;
|
||||||
@@ -465,6 +465,7 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[self.yuv444Selector setSelectedSegmentIndex:currentSettings.enableYUV444 ? 1 : 0];
|
[self.yuv444Selector setSelectedSegmentIndex:currentSettings.enableYUV444 ? 1 : 0];
|
||||||
|
[self.pipSelector setSelectedSegmentIndex:currentSettings.enablePIP ? 1 : 0];
|
||||||
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlayLevel.intValue];
|
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlayLevel.intValue];
|
||||||
[self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0];
|
[self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0];
|
||||||
[self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
|
[self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
|
||||||
@@ -477,6 +478,8 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
[self.resolutionSelector addTarget:self action:@selector(newResolutionChosen) forControlEvents:UIControlEventValueChanged];
|
[self.resolutionSelector addTarget:self action:@selector(newResolutionChosen) forControlEvents:UIControlEventValueChanged];
|
||||||
[self.framerateSelector setSelectedSegmentIndex:framerate];
|
[self.framerateSelector setSelectedSegmentIndex:framerate];
|
||||||
[self.framerateSelector addTarget:self action:@selector(updateBitrate) forControlEvents:UIControlEventValueChanged];
|
[self.framerateSelector addTarget:self action:@selector(updateBitrate) forControlEvents:UIControlEventValueChanged];
|
||||||
|
NSInteger motionMode = [currentSettings.motionMode integerValue];
|
||||||
|
[self.motionModeSelector setSelectedSegmentIndex:motionMode];
|
||||||
[self.bitrateSlider setMinimumValue:0];
|
[self.bitrateSlider setMinimumValue:0];
|
||||||
[self.bitrateSlider setMaximumValue:(sizeof(bitrateTable) / sizeof(*bitrateTable)) - 1];
|
[self.bitrateSlider setMaximumValue:(sizeof(bitrateTable) / sizeof(*bitrateTable)) - 1];
|
||||||
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
|
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
|
||||||
@@ -1054,6 +1057,7 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
NSInteger height = [self getChosenStreamHeight];
|
NSInteger height = [self getChosenStreamHeight];
|
||||||
NSInteger width = [self getChosenStreamWidth];
|
NSInteger width = [self getChosenStreamWidth];
|
||||||
NSInteger onscreenControls = [self.onscreenControlSelector selectedSegmentIndex];
|
NSInteger onscreenControls = [self.onscreenControlSelector selectedSegmentIndex];
|
||||||
|
NSInteger motionMode = [self.motionModeSelector selectedSegmentIndex];
|
||||||
NSInteger keyboardToggleFingers = (uint16_t)self.keyboardToggleFingerNumSlider.value;
|
NSInteger keyboardToggleFingers = (uint16_t)self.keyboardToggleFingerNumSlider.value;
|
||||||
NSInteger oscLayoutToolFingers = (uint16_t)self->oscLayoutFingers;
|
NSInteger oscLayoutToolFingers = (uint16_t)self->oscLayoutFingers;
|
||||||
// NSLog(@"saveSettings keyboardToggleFingers %d", (uint16_t)keyboardToggleFingers);
|
// NSLog(@"saveSettings keyboardToggleFingers %d", (uint16_t)keyboardToggleFingers);
|
||||||
@@ -1076,6 +1080,7 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
BOOL audioOnPC = [self.audioOnPCSelector selectedSegmentIndex] == 1;
|
BOOL audioOnPC = [self.audioOnPCSelector selectedSegmentIndex] == 1;
|
||||||
uint32_t preferredCodec = [self getChosenCodecPreference];
|
uint32_t preferredCodec = [self getChosenCodecPreference];
|
||||||
BOOL enableYUV444 = [self.yuv444Selector selectedSegmentIndex] == 1;
|
BOOL enableYUV444 = [self.yuv444Selector selectedSegmentIndex] == 1;
|
||||||
|
BOOL enablePIP = [self.pipSelector selectedSegmentIndex] == 1;
|
||||||
BOOL btMouseSupport = [self.btMouseSelector selectedSegmentIndex] == 1;
|
BOOL btMouseSupport = [self.btMouseSelector selectedSegmentIndex] == 1;
|
||||||
BOOL useFramePacing = [self.framePacingSelector selectedSegmentIndex] == 1;
|
BOOL useFramePacing = [self.framePacingSelector selectedSegmentIndex] == 1;
|
||||||
// BOOL absoluteTouchMode = [self.touchModeSelector selectedSegmentIndex] == 1;
|
// BOOL absoluteTouchMode = [self.touchModeSelector selectedSegmentIndex] == 1;
|
||||||
@@ -1087,12 +1092,14 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
NSInteger resolutionSelected = [self.resolutionSelector selectedSegmentIndex];
|
NSInteger resolutionSelected = [self.resolutionSelector selectedSegmentIndex];
|
||||||
NSInteger externalDisplayMode = [self.externalDisplayModeSelector selectedSegmentIndex];
|
NSInteger externalDisplayMode = [self.externalDisplayModeSelector selectedSegmentIndex];
|
||||||
NSInteger localMousePointerMode = [self.localMousePointerModeSelector selectedSegmentIndex];
|
NSInteger localMousePointerMode = [self.localMousePointerModeSelector selectedSegmentIndex];
|
||||||
|
NSLog(@"saving motionMode: %ld", motionMode);
|
||||||
[dataMan saveSettingsWithBitrate:_bitrate
|
[dataMan saveSettingsWithBitrate:_bitrate
|
||||||
framerate:framerate
|
framerate:framerate
|
||||||
height:height
|
height:height
|
||||||
width:width
|
width:width
|
||||||
audioConfig:2 // Stereo
|
audioConfig:2 // Stereo
|
||||||
onscreenControls:onscreenControls
|
onscreenControls:onscreenControls
|
||||||
|
motionMode:motionMode
|
||||||
keyboardToggleFingers:keyboardToggleFingers
|
keyboardToggleFingers:keyboardToggleFingers
|
||||||
oscLayoutToolFingers:oscLayoutToolFingers
|
oscLayoutToolFingers:oscLayoutToolFingers
|
||||||
slideToSettingsScreenEdge:slideToSettingsScreenEdge
|
slideToSettingsScreenEdge:slideToSettingsScreenEdge
|
||||||
@@ -1111,6 +1118,7 @@ BOOL isCustomResolution(CGSize res) {
|
|||||||
audioOnPC:audioOnPC
|
audioOnPC:audioOnPC
|
||||||
preferredCodec:preferredCodec
|
preferredCodec:preferredCodec
|
||||||
enableYUV444:enableYUV444
|
enableYUV444:enableYUV444
|
||||||
|
enablePIP:enablePIP
|
||||||
useFramePacing:useFramePacing
|
useFramePacing:useFramePacing
|
||||||
enableHdr:enableHdr
|
enableHdr:enableHdr
|
||||||
btMouseSupport:btMouseSupport
|
btMouseSupport:btMouseSupport
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
//
|
|
||||||
// StreamFrameViewController.h
|
|
||||||
// Moonlight
|
|
||||||
//
|
|
||||||
// Created by Diego Waxemberg on 1/18/14.
|
|
||||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "Connection.h"
|
|
||||||
#import "StreamConfiguration.h"
|
|
||||||
#import "StreamView.h"
|
|
||||||
#import "LayoutOnScreenControlsViewController.h"
|
|
||||||
#import "MainFrameViewController.h"
|
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
#if TARGET_OS_TV
|
|
||||||
@import GameController;
|
|
||||||
|
|
||||||
@interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate>
|
|
||||||
#else
|
|
||||||
@interface StreamFrameViewController : UIViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate>
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@property (nonatomic) StreamConfiguration* streamConfig;
|
|
||||||
@property (nonatomic, assign) MainFrameViewController *mainFrameViewcontroller;
|
|
||||||
|
|
||||||
|
|
||||||
-(void)updatePreferredDisplayMode:(BOOL)streamActive;
|
|
||||||
|
|
||||||
@end
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Moonlight-ZWM</string>
|
<string>VoidLink</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>IDESourceControlProjectFavoriteDictionaryKey</key>
|
|
||||||
<false/>
|
|
||||||
<key>IDESourceControlProjectIdentifier</key>
|
|
||||||
<string>054A62F3-1197-45FB-A607-EA7922396E24</string>
|
|
||||||
<key>IDESourceControlProjectName</key>
|
|
||||||
<string>Limelight</string>
|
|
||||||
<key>IDESourceControlProjectOriginsDictionary</key>
|
|
||||||
<dict>
|
|
||||||
<key>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</key>
|
|
||||||
<string>github.com:limelight-stream/limelight-ios.git</string>
|
|
||||||
<key>EB99D631D5149240E91F8D168F0F298D7A5BAA89</key>
|
|
||||||
<string>github.com:limelight-stream/limelight-common-c.git</string>
|
|
||||||
</dict>
|
|
||||||
<key>IDESourceControlProjectPath</key>
|
|
||||||
<string>Limelight.xcodeproj</string>
|
|
||||||
<key>IDESourceControlProjectRelativeInstallPathDictionary</key>
|
|
||||||
<dict>
|
|
||||||
<key>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</key>
|
|
||||||
<string>../..</string>
|
|
||||||
<key>EB99D631D5149240E91F8D168F0F298D7A5BAA89</key>
|
|
||||||
<string>../../limelight-common-c</string>
|
|
||||||
</dict>
|
|
||||||
<key>IDESourceControlProjectURL</key>
|
|
||||||
<string>github.com:limelight-stream/limelight-ios.git</string>
|
|
||||||
<key>IDESourceControlProjectVersion</key>
|
|
||||||
<integer>111</integer>
|
|
||||||
<key>IDESourceControlProjectWCCIdentifier</key>
|
|
||||||
<string>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</string>
|
|
||||||
<key>IDESourceControlProjectWCConfigurations</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
|
||||||
<string>public.vcs.git</string>
|
|
||||||
<key>IDESourceControlWCCIdentifierKey</key>
|
|
||||||
<string>EB99D631D5149240E91F8D168F0F298D7A5BAA89</string>
|
|
||||||
<key>IDESourceControlWCCName</key>
|
|
||||||
<string>limelight-common-c</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
|
||||||
<string>public.vcs.git</string>
|
|
||||||
<key>IDESourceControlWCCIdentifierKey</key>
|
|
||||||
<string>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</string>
|
|
||||||
<key>IDESourceControlWCCName</key>
|
|
||||||
<string>limelight-ios</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>IDESourceControlProjectFavoriteDictionaryKey</key>
|
|
||||||
<false/>
|
|
||||||
<key>IDESourceControlProjectIdentifier</key>
|
|
||||||
<string>AD7C6582-F39A-441A-A7FD-9EBF143920BA</string>
|
|
||||||
<key>IDESourceControlProjectName</key>
|
|
||||||
<string>Moonlight</string>
|
|
||||||
<key>IDESourceControlProjectOriginsDictionary</key>
|
|
||||||
<dict>
|
|
||||||
<key>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</key>
|
|
||||||
<string>github.com:limelight-stream/limelight-ios.git</string>
|
|
||||||
<key>EB99D631D5149240E91F8D168F0F298D7A5BAA89</key>
|
|
||||||
<string>github.com:limelight-stream/limelight-common-c.git</string>
|
|
||||||
</dict>
|
|
||||||
<key>IDESourceControlProjectPath</key>
|
|
||||||
<string>Moonlight.xcodeproj</string>
|
|
||||||
<key>IDESourceControlProjectRelativeInstallPathDictionary</key>
|
|
||||||
<dict>
|
|
||||||
<key>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</key>
|
|
||||||
<string>../..</string>
|
|
||||||
<key>EB99D631D5149240E91F8D168F0F298D7A5BAA89</key>
|
|
||||||
<string>../../limelight-common-c</string>
|
|
||||||
</dict>
|
|
||||||
<key>IDESourceControlProjectURL</key>
|
|
||||||
<string>github.com:limelight-stream/limelight-ios.git</string>
|
|
||||||
<key>IDESourceControlProjectVersion</key>
|
|
||||||
<integer>111</integer>
|
|
||||||
<key>IDESourceControlProjectWCCIdentifier</key>
|
|
||||||
<string>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</string>
|
|
||||||
<key>IDESourceControlProjectWCConfigurations</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
|
||||||
<string>public.vcs.git</string>
|
|
||||||
<key>IDESourceControlWCCIdentifierKey</key>
|
|
||||||
<string>EB99D631D5149240E91F8D168F0F298D7A5BAA89</string>
|
|
||||||
<key>IDESourceControlWCCName</key>
|
|
||||||
<string>limelight-common-c</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
|
||||||
<string>public.vcs.git</string>
|
|
||||||
<key>IDESourceControlWCCIdentifierKey</key>
|
|
||||||
<string>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</string>
|
|
||||||
<key>IDESourceControlWCCName</key>
|
|
||||||
<string>moonlight-ios</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>IDESourceControlProjectFavoriteDictionaryKey</key>
|
|
||||||
<false/>
|
|
||||||
<key>IDESourceControlProjectIdentifier</key>
|
|
||||||
<string>6524503E-ED62-4E8E-8A67-B6DFB5A532FD</string>
|
|
||||||
<key>IDESourceControlProjectName</key>
|
|
||||||
<string>project</string>
|
|
||||||
<key>IDESourceControlProjectOriginsDictionary</key>
|
|
||||||
<dict>
|
|
||||||
<key>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</key>
|
|
||||||
<string>github.com:limelight-stream/limelight-ios.git</string>
|
|
||||||
<key>EB99D631D5149240E91F8D168F0F298D7A5BAA89</key>
|
|
||||||
<string>github.com:limelight-stream/limelight-common-c.git</string>
|
|
||||||
</dict>
|
|
||||||
<key>IDESourceControlProjectPath</key>
|
|
||||||
<string>Moonlight.xcodeproj/project.xcworkspace</string>
|
|
||||||
<key>IDESourceControlProjectRelativeInstallPathDictionary</key>
|
|
||||||
<dict>
|
|
||||||
<key>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</key>
|
|
||||||
<string>../..</string>
|
|
||||||
<key>EB99D631D5149240E91F8D168F0F298D7A5BAA89</key>
|
|
||||||
<string>../..limelight-common-c/</string>
|
|
||||||
</dict>
|
|
||||||
<key>IDESourceControlProjectURL</key>
|
|
||||||
<string>github.com:limelight-stream/limelight-ios.git</string>
|
|
||||||
<key>IDESourceControlProjectVersion</key>
|
|
||||||
<integer>111</integer>
|
|
||||||
<key>IDESourceControlProjectWCCIdentifier</key>
|
|
||||||
<string>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</string>
|
|
||||||
<key>IDESourceControlProjectWCConfigurations</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
|
||||||
<string>public.vcs.git</string>
|
|
||||||
<key>IDESourceControlWCCIdentifierKey</key>
|
|
||||||
<string>EB99D631D5149240E91F8D168F0F298D7A5BAA89</string>
|
|
||||||
<key>IDESourceControlWCCName</key>
|
|
||||||
<string>limelight-common-c</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
|
||||||
<string>public.vcs.git</string>
|
|
||||||
<key>IDESourceControlWCCIdentifierKey</key>
|
|
||||||
<string>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</string>
|
|
||||||
<key>IDESourceControlWCCName</key>
|
|
||||||
<string>limelight-ios</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,24 +1,42 @@
|
|||||||
# Moonlight iOS/tvOS
|
# VoidLink 已上架App Store.
|
||||||
|
# VoidLink is available on App Store
|
||||||
|
### [https://apps.apple.com/app/voidlink/id6747717070](https://apps.apple.com/cn/app/voidlink/id6747717070)
|
||||||
|
|
||||||
[](https://ci.appveyor.com/project/cgutman/moonlight-ios/branch/master)
|
<br>
|
||||||
|
|
||||||
[Moonlight for iOS/tvOS](https://moonlight-stream.org) is an open source client for [Sunshine](https://github.com/LizardByte/Sunshine) and NVIDIA GameStream. Moonlight for iOS/tvOS allows you to stream your full collection of games and apps from your powerful desktop computer to your iOS device or Apple TV.
|
# 代码 Coding
|
||||||
|
- 代码提交在`Integration`分支。
|
||||||
|
- For latest coding commits, go to branch `Integration`.
|
||||||
|
|
||||||
Moonlight also has a [PC client](https://github.com/moonlight-stream/moonlight-qt) and [Android client](https://github.com/moonlight-stream/moonlight-android).
|
<br>
|
||||||
|
|
||||||
Check out [the Moonlight wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed project information, setup guide, or troubleshooting steps.
|
# 关于VoidLink. About VoidLink
|
||||||
|
- 本项目最初基于开源项目 [moonlight-iOS] fork 而来。在此基础上,True砖家(True Zhuanjia)@ Bilibili 及其他社区开发者对项目进行了大量重构、重新设计与功能扩展,包括全新的用户界面和显著增强的功能特性。<br>我们对 moonlight-iOS 开发者的开创性工作表示衷心感谢。 <br><br>
|
||||||
|
- VoidLink was originally forked from the open-source project [moonlight-iOS], but has since been extensively reworked, redesigned, and expanded by True砖家 (True Zhuanjia) @ Bilibili and other community developers. These contributions include a completely new user interface and significant enhancements to the application's functionality.<br>We gratefully acknowledge the foundational work of the moonlight-iOS developers.
|
||||||
|
|
||||||
[](https://apps.apple.com/us/app/moonlight-game-streaming/id1000551566)
|
<br>
|
||||||
|
|
||||||
## Building
|
# App Store 分发一次性收费声明.
|
||||||
* Install Xcode from the [App Store page](https://apps.apple.com/us/app/xcode/id497799835)
|
# Statement on One-Time App Store Distribution Fee.
|
||||||
* Run `git clone --recursive https://github.com/moonlight-stream/moonlight-ios.git`
|
- VoidLink 的 App Store 安装费用用于覆盖通过 Apple 框架进行安全可靠应用分发的成本。同时也支持项目维护者进行持续的开发、维护,以及公众用户访问并下载官方签名版本。感谢您对项目的支持,帮助我们持续改进和优化。<br><br>
|
||||||
* If you've already clone the repo without `--recursive`, run `git submodule update --init --recursive`
|
- The App Store fee for VoidLink covers the cost of secure and trusted distribution using Apple's infrastructure. It supports ongoing development, maintenance, and access to the official signed build. Thank you for supporting the project and helping sustain ongoing improvements.
|
||||||
* Open Moonlight.xcodeproj in Xcode
|
|
||||||
* To run on a real device, you will need to locally modify the signing options:
|
<br>
|
||||||
* Click on "Moonlight" at the top of the left sidebar
|
|
||||||
* Click on the "Signing & Capabilities" tab
|
# 开发者B站号. Developer on Bilibili
|
||||||
* Under "Targets", select "Moonlight" (for iOS/iPadOS) or "Moonlight TV" (for tvOS)
|
|
||||||
* In the "Team" dropdown, select your name. If your name doesn't appear, you may need to sign into Xcode with your Apple account.
|
如果你在用Bilibili, 请关注`True砖家`,了解该fork的最新消息。 <br>
|
||||||
* Change the "Bundle Identifier" to something different. You can add your name or some random letters to make it unique.
|
If you are on Bilibili, subscribe `True砖家` to get the latest news of this fork: <br>
|
||||||
* Now you can select your Apple device in the top bar as a target and click the Play button to run.
|
https://b23.tv/A0F9v7n
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
# 贡献者 Contributors
|
||||||
|
[@TrueZhuangJia](https://github.com/TrueZhuangJia) <br>
|
||||||
|
[@All contributors from moonlight-iOS](https://github.com/moonlight-stream/moonlight-ios/graphs/contributors) <br>
|
||||||
|
[@stefanilijev97](https://github.com/stefanilijev97/stefanilijev97) <br>
|
||||||
|
[@Acaki](https://github.com/Acaki) <br>
|
||||||
|
[@seastwood](https://github.com/seastwood) <br>
|
||||||
|
[@Danos0100](https://github.com/Danos0100) <br>
|
||||||
|
[@xzzpig](https://github.com/xzzpig) <br>
|
||||||
|
[@King0fSpace](https://github.com/King0fSpace) <br>
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
<Workspace
|
<Workspace
|
||||||
version = "1.0">
|
version = "1.0">
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "self:/Users/admin/moonlight-ios/Moonlight-ZWM.xcodeproj">
|
location = "self:">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"originHash" : "d1fc50a28d75017e32553df2b233f8a7e2cf1542da32728ce6722b200f745608",
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "cocoalumberjack",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "a9ed4b6f9bdedce7d77046f43adfb8ce1fd54114",
|
||||||
|
"version" : "3.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "openssl-package",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/krzyzanowskim/OpenSSL-Package.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "b9eb055fdf73e595cb4b9f665d13dc85975bf80a",
|
||||||
|
"version" : "3.3.2000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "svgkit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SVGKit/SVGKit.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "58152b9f7c85eab239160b36ffdfd364aa43d666",
|
||||||
|
"version" : "3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-collections",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-collections.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0",
|
||||||
|
"version" : "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-log",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-log",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "bc386b95f2a16ccd0150a8235e7c69eab2b866ca",
|
||||||
|
"version" : "1.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 3
|
||||||
|
}
|
||||||