1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
|
/**
* @file llfloaterpreference.cpp
* @brief Global preferences with and without persistence.
*
* $LicenseInfo:firstyear=2002&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2010, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
/*
* App-wide preferences. Note that these are not per-user,
* because we need to load many preferences before we have
* a login name.
*/
#include "llviewerprecompiledheaders.h"
#include "llpanelpreferencegamecontrol.h"
#include "llbutton.h"
#include "llcheckboxctrl.h"
#include "llcombobox.h"
#include "llsliderctrl.h"
#include "lltabcontainer.h"
#include "llspinctrl.h"
#include "lltextbox.h"
namespace
{
// Singleton instance pointer - only one game control panel exists at a time
static LLPanelPreferenceGameControl* sGameControlPanel { nullptr };
// Track current UI selection state for input/output channel assignment.
// When user clicks a cell to change its mapping, these track which cell is being edited.
static LLScrollListCtrl* sSelectedGrid { nullptr };
static LLScrollListItem* sSelectedItem { nullptr };
static LLScrollListCell* sSelectedCell { nullptr };
static S32 sSelectedRow { -1 };
static S32 sSelectedCol { -1 };
static LLRect sSelectedOpenRect;
// The combobox popup currently overlaying sSelectedCell (one of the four
// action-mapping/output selectors below). Stashed so applyGameControlInput()
// can drive it (and reuse onCommitInputChannel()) without having to re-derive
// which selector initCombobox() chose.
static LLComboBox* sSelectedCombobox { nullptr };
// The selector combobox overlays the edited cell, but its drop-down button
// texture is semi-transparent, so the cell's own text shows through and
// "doubles" with the selector's label. While a selector is open we blank
// the cell's text and stash its value here, restoring it when the selector
// closes (see initCombobox / clearSelectionState).
static LLSD sSelectedCellValue;
// Selector items use the label "None" for the unmapped entry.
// Table cells should render that as a blank cell rather than the word.
const std::string NONE_LABEL("None");
// Kinds of action mapping, used both as the LLGameControl mapping key and as
// the block discriminator stored in each action-table row's value.
const std::string MODE_INPUT_TYPE_AXES("Axes");
const std::string MODE_INPUT_TYPE_BUTTONS("Buttons");
const std::string MODE_INPUT_TYPE_AXES_INVERT("AxesInvert");
LLSD blankIfNone(const LLSD& label)
{
return label.asString() == NONE_LABEL ? LLSD() : label;
}
// SecondLife-game-control glyph(s) for a canonical controller input value.
// Codepoints are from the SecondLife-game-control package's promptfont.h,
// preferring the generic vendor-neutral "gamepad"/"dpad"/"analog" sets and
// falling back to the Xbox set for shoulders/triggers (no generic equivalents
// exist). Rendered with the "SecondLife-game-control" family declared in
// fonts.xml. Inputs with no entry (e.g. BUTTON_21..31, *_NONE) return empty
// and keep their text label.
std::string promptFontGlyph(const std::string& input_value)
{
static const std::map<std::string, LLWString> sGlyphs = {
// Face buttons
{ "BUTTON_SOUTH", { 0x21A7 } }, // PF_GAMEPAD_A (bottom)
{ "BUTTON_EAST", { 0x21A6 } }, // PF_GAMEPAD_B (right)
{ "BUTTON_WEST", { 0x21A4 } }, // PF_GAMEPAD_X (left)
{ "BUTTON_NORTH", { 0x21A5 } }, // PF_GAMEPAD_Y (top)
// D-pad
{ "BUTTON_DPAD_UP", { 0x219F } }, // PF_DPAD_UP
{ "BUTTON_DPAD_DOWN", { 0x21A1 } }, // PF_DPAD_DOWN
{ "BUTTON_DPAD_LEFT", { 0x219E } }, // PF_DPAD_LEFT
{ "BUTTON_DPAD_RIGHT", { 0x21A0 } }, // PF_DPAD_RIGHT
// Shoulders / stick clicks
{ "BUTTON_LEFT_SHOULDER", { 0x2198 } }, // PF_XBOX_LEFT_SHOULDER
{ "BUTTON_RIGHT_SHOULDER", { 0x2199 } }, // PF_XBOX_RIGHT_SHOULDER
{ "BUTTON_LEFT_STICK", { 0x21BA } }, // PF_ANALOG_L_CLICK (L3)
{ "BUTTON_RIGHT_STICK", { 0x21BB } }, // PF_ANALOG_R_CLICK (R3)
// Menu cluster
{ "BUTTON_SELECT", { 0x21F7 } }, // PF_GAMEPAD_SELECT (menu left)
{ "BUTTON_HOME", { 0x21F9 } }, // PF_GAMEPAD_HOME (menu middle)
{ "BUTTON_START", { 0x21F8 } }, // PF_GAMEPAD_START (menu right)
// Misc / paddles / touchpad
{ "BUTTON_15", { 0x2212 } }, // PF_GAMEPAD_M1 (Misc1)
{ "BUTTON_PADDLE1", { 0x2277 } }, // PF_GAMEPAD_R4
{ "BUTTON_PADDLE2", { 0x2276 } }, // PF_GAMEPAD_L4
{ "BUTTON_PADDLE3", { 0x2279 } }, // PF_GAMEPAD_R5
{ "BUTTON_PADDLE4", { 0x2278 } }, // PF_GAMEPAD_L5
{ "BUTTON_TOUCHPAD", { 0x21E7 } }, // PF_SONY_TOUCHPAD
// Axes
{ "AXIS_LEFTX", { 0x21C4 } }, // PF_ANALOG_L_LEFT_RIGHT
{ "AXIS_LEFTY", { 0x21C5 } }, // PF_ANALOG_L_UP_DOWN
{ "AXIS_RIGHTX", { 0x21C6 } }, // PF_ANALOG_R_LEFT_RIGHT
{ "AXIS_RIGHTY", { 0x21F5 } }, // PF_ANALOG_R_UP_DOWN
{ "AXIS_LEFT_TRIGGER", { 0x2196 } }, // PF_XBOX_LEFT_TRIGGER
{ "AXIS_RIGHT_TRIGGER", { 0x2197 } }, // PF_XBOX_RIGHT_TRIGGER
// Combined trigger pair: show both trigger glyphs side by side.
{ "AXIS_TRIGGERS", { 0x2196, 0x2197 } },
};
auto it = sGlyphs.find(input_value);
if (it == sGlyphs.end())
{
return LLStringUtil::null;
}
return wstring_to_utf8str(it->second);
}
// The SecondLife-game-control glyphs live on real Unicode arrow codepoints,
// so they must be drawn with the dedicated SecondLife-game-control family
// rather than the default text font. Sized larger than the SansSerif rows
// so the icons read clearly.
const LLFontGL* promptFontForCell()
{
return LLFontGL::getFont(LLFontDescriptor("SecondLife-game-control", "Huge", 0));
}
}
static LLRect computeCellScreenRect(LLScrollListCtrl* grid, LLView* relative_to, S32 row_index, S32 col_index);
// Static entry point called when device list changes (device connected/disconnected).
// Delegates to the singleton instance if it exists.
void LLPanelPreferenceGameControl::updateDeviceList()
{
if (sGameControlPanel)
{
sGameControlPanel->updateDeviceListInternal();
}
}
// Constructor - registers this instance as the singleton
LLPanelPreferenceGameControl::LLPanelPreferenceGameControl()
{
sGameControlPanel = this;
}
// Destructor - clears singleton pointer
LLPanelPreferenceGameControl::~LLPanelPreferenceGameControl()
{
sGameControlPanel = nullptr;
}
static LLPanelInjector<LLPanelPreferenceGameControl> t_pref_game_control("panel_preference_game_control");
// Snapshot the panel's UI state as the single "GameControl" setting.
// The returned map can be persisted (saveSettings) or applied directly to
// LLGameControl runtime state via LLGameControl::applySettingsFromLLSD().
LLSD LLPanelPreferenceGameControl::getSettingsAsLLSD()
{
// Fold each device's current options into GameControl/Devices/<guid>/Config.
// The global per-mode action mappings are already live in LLGameControl (edited
// via its accessors), so snapshotting the whole GameControl structure captures both.
for (auto& [guid, device] : mDeviceOptions)
{
// force_empty=true: persist every known device's name even when its
// options are still at defaults, so "Show all known devices" can find
// it again after it's unplugged.
device.settings = device.options.saveToString(device.name, true);
LLGameControl::setDeviceConfig(guid, device.settings);
}
LLSD result = LLSD::emptyMap();
result["GameControl"] = LLGameControl::getGameControlSettings();
return result;
}
// Saves current UI state to settings by pushing the "GameControl" snapshot
// (global action mappings + per-device Config) into gSavedSettings.
void LLPanelPreferenceGameControl::saveSettings()
{
LLPanelPreference::saveSettings();
if (mOrigSettings.isEmpty())
{
rememberOriginalSettings();
}
// Push every entry of the snapshot into gSavedSettings, also tracking
// the new value in mSavedValues so the base panel's accounting stays in sync.
LLSD settings = getSettingsAsLLSD();
for (auto it = settings.beginMap(); it != settings.endMap(); ++it)
{
if (LLControlVariable* control = gSavedSettings.getControl(it->first))
{
control->set(it->second);
mSavedValues[control] = control->getValue();
}
}
}
// Handles row selection in the Actions table. If the user clicked the editable
// Input column of a real (non-spacer) row, shows the appropriate input selector.
void LLPanelPreferenceGameControl::onGridSelect(LLUICtrl* ctrl)
{
clearSelectionState();
LLScrollListCtrl* table = dynamic_cast<LLScrollListCtrl*>(ctrl);
if (!table || !table->getEnabled())
return;
// Deselect everything in the other scroll_list
LLScrollListCtrl* sibling = (table == mActionMappingsAxes) ? mActionMappingsButtons : mActionMappingsAxes;
sibling->deselectAllItems(true);
if (LLScrollListItem* item = table->getFirstSelected())
{
if (table == mActionMappingsAxes)
{
// Always sync the Invert checkbox -- clicking it selects the row (firing
// this same commit callback) but doesn't itself persist the new value.
// Only meaningful when the row has a real axis mapped (the checkbox is
// disabled and inert otherwise, matching populateActionMappings()).
std::string mode = currentEditMode();
LLSD row_value = item->getValue();
std::string action = row_value["action"].asString();
LLSD mapping = LLGameControl::getModeMapping(mode, MODE_INPUT_TYPE_AXES);
std::string input_value = mapping.has(action) ? mapping[action].asString() : LLStringUtil::null;
std::string none_value = mAxisInputSelector->getAllData().back()->getValue().asString();
if (!input_value.empty() && input_value != none_value)
{
S32 invert_col = mActionMappingsAxes->getColumn("invert")->mIndex;
LLGameControl::setAxisInvert(mode, action, item->getColumn(invert_col)->getValue().asBoolean());
}
}
// Try to show combobox for editing; if not applicable, deselect
if (initCombobox(item, table))
return;
table->deselectAllItems();
}
}
// Initializes and displays the appropriate combobox over the selected cell.
// Chooses the combobox and editable column from the table and (for the Actions
// table) the row's block input_type. Returns true if a combobox was shown, false if the
// click wasn't on an editable cell.
bool LLPanelPreferenceGameControl::initCombobox(LLScrollListItem* item, LLScrollListCtrl* grid)
{
LLComboBox* combobox = nullptr;
S32 col = -1;
if (grid == mActionMappingsAxes || grid == mActionMappingsButtons)
{
// Two editable columns map to the same input: the SecondLife-game-control
// icon column (1) opens the glyph selector, the text description column
// (2) opens the text selector. Both commit through onCommitInputChannel,
// which rebuilds the row so the icon and description update together.
LLSD row_value = item->getValue();
if (!row_value.isMap())
return false;
bool axes = (grid == mActionMappingsAxes);
S32 sel = item->getSelectedCell();
if (sel == 1)
{
combobox = axes ? mAxisInputGlyphSelector : mButtonInputGlyphSelector;
}
else if (sel == 2)
{
combobox = axes ? mAxisInputSelector : mButtonInputSelector;
}
else
{
return false;
}
col = sel;
}
else if (grid == mAxisChannels)
{
// Two editable columns map to the same output: the SecondLife-game-control
// icon column ("output") opens the glyph selector, the text column
// ("output_description") opens the text selector. Both commit through
// onCommitInputChannel, which refreshes both cells so the icon and
// description stay in sync. The Input columns are the fixed physical axis
// and are not editable.
S32 sel = item->getSelectedCell();
S32 desc_col = grid->getColumn("output_description")->mIndex;
S32 glyph_col = grid->getColumn("output")->mIndex;
if (sel == glyph_col)
{
combobox = mAxisOutputGlyphSelector;
}
else if (sel == desc_col)
{
combobox = mAxisOutputSelector;
}
else
{
return false;
}
col = sel;
}
else if (grid == mButtonChannels)
{
// Two editable columns map to the same output: the SecondLife-game-control
// icon column ("output") opens the glyph selector, the text column
// ("output_description") opens the text selector. Both commit through
// onCommitInputChannel, which refreshes both cells so the icon and
// description stay in sync. The Input columns are the fixed physical
// button and are not editable.
S32 sel = item->getSelectedCell();
S32 desc_col = grid->getColumn("output_description")->mIndex;
S32 glyph_col = grid->getColumn("output")->mIndex;
if (sel == glyph_col)
{
combobox = mButtonInputGlyphSelector;
}
else if (sel == desc_col)
{
combobox = mButtonInputSelector;
}
else
{
return false;
}
col = sel;
}
if (!combobox)
return false;
LLScrollListText* cell = dynamic_cast<LLScrollListText*>(item->getColumn(col));
if (!cell)
return false;
// compute new rect for combobox
S32 row_index = grid->getItemIndex(item);
fitInRect(combobox, grid, row_index, col);
sSelectedRow = row_index;
sSelectedCol = col;
sSelectedOpenRect = computeCellScreenRect(grid, combobox->getParent(), row_index, col);
// Pre-select the item matching the cell's current input.
std::string value;
if (grid == mActionMappingsAxes || grid == mActionMappingsButtons)
{
// Use the action's stored mapping value directly, so pre-selection is
// correct even when the icon cell is blank (input has no glyph).
LLSD row_value = item->getValue();
LLSD mapping = LLGameControl::getModeMapping(currentEditMode(), row_value["input_type"].asString());
std::string action = row_value["action"].asString();
value = mapping.has(action) ? mapping[action].asString() : LLStringUtil::null;
}
else
{
// Device tables: match the cell's current text against the selector items
// (works regardless of the value naming scheme).
std::string cell_value = cell->getValue();
for (const LLScrollListItem* combo_item : combobox->getAllData())
{
if (combo_item->getColumn(0)->getValue().asString() == cell_value)
{
value = combo_item->getValue().asString();
break;
}
}
}
if (value.empty())
{
// No match (e.g. a blank "None" cell): select the last item, which is "None".
value = combobox->getAllData().back()->getValue().asString();
}
combobox->setValue(value);
combobox->setVisible(true);
combobox->showList();
sSelectedGrid = grid;
sSelectedItem = item;
sSelectedCell = cell;
sSelectedCombobox = combobox;
// Hide the cell's text under the (translucent) selector so it doesn't
// double with the selector's label; clearSelectionState() restores it.
sSelectedCellValue = cell->getValue();
cell->setValue(LLSD());
return true;
}
// Called when the user selects a value from an input/output selector combobox.
// The originating grid determines the meaning: Actions table -> action mapping;
// axis/button channels table -> per-device physical-to-canonical remap.
void LLPanelPreferenceGameControl::onCommitInputChannel(LLUICtrl* ctrl)
{
if (!sSelectedGrid || !sSelectedItem || !sSelectedCell)
return;
LLComboBox* combobox = dynamic_cast<LLComboBox*>(ctrl);
llassert(combobox);
if (!combobox)
return;
// Editing an action mapping can change whether the input cell shows a text
// label or a SecondLife-game-control glyph, which means recreating the cell
// with the right font; remember to rebuild the action tables once the commit
// is applied.
bool action_grid_edited = (sSelectedGrid == mActionMappingsAxes || sSelectedGrid == mActionMappingsButtons);
// Editing an axis- or button-channels output likewise changes both its text and
// icon cells, so both are refreshed from the device map once the commit is applied.
bool axis_channels_edited = (sSelectedGrid == mAxisChannels);
bool button_channels_edited = (sSelectedGrid == mButtonChannels);
if (sSelectedGrid == mActionMappingsAxes || sSelectedGrid == mActionMappingsButtons)
{
// The edited row identifies the action + block; the combobox supplies the input.
LLSD row_value = sSelectedItem->getValue();
std::string mode = currentEditMode();
std::string input_type = row_value["input_type"].asString();
std::string action = row_value["action"].asString();
const LLComboBox* input_selector = (input_type == MODE_INPUT_TYPE_AXES) ? mAxisInputSelector : mButtonInputSelector;
std::string input_value = combobox->getValue().asString();
std::string input_label = combobox->getSelectedItemLabel();
if (input_label != NONE_LABEL && isFlycamRollDisallowed(mode, action))
{
// Roll/Unroll are unavailable while "Allow Roll" is off: refuse to
// create a new mapping. Clearing to None (input_label == NONE_LABEL)
// still falls through below, so an existing mapping remains removable.
// clearSelectionState() (below) restores the cell's pre-edit text.
}
else
{
if (input_label != NONE_LABEL)
{
// An input can drive at most one action within a block: clear it elsewhere.
removeDuplicateActionInput(mode, input_type, action, input_value, input_selector);
}
// Store the mapping directly in LLGameControl's live GameControl settings.
// gSavedSettings is updated later via saveSettings() when the user clicks OK.
LLGameControl::updateModeMapping(mode, input_type, action, input_value);
sSelectedCell->setValue(blankIfNone(input_label));
}
}
else if (sSelectedGrid == mAxisChannels)
{
// The row is a physical axis; the selected item's value names the output it
// maps to: an individual canonical axis, "Triggers left/right" (the fan-out
// pair), or "None". Output codes are resolved by LLGameControl so the panel
// stays agnostic of the axis-map encoding.
S32 axis = mAxisChannels->getItemIndex(sSelectedItem);
std::string output_name = combobox->getValue().asString();
LLGameControl::Options& options = getSelectedDeviceOptions();
options.getAxisMap()[axis] = LLGameControl::axisOutputFromName(output_name);
LLGameControl::setDeviceOptions(mSelectedDeviceGUID, options);
// Both output cells (icon + description) are re-rendered from the actual
// map by populateAxisChannelsCells() below.
}
else if (sSelectedGrid == mButtonChannels)
{
// The row is a physical button; the selected item's index is the canonical
// button it maps to. "None" (index >= NUM_BUTTONS) is not a valid output.
S32 button = mButtonChannels->getItemIndex(sSelectedItem);
S32 output = combobox->getCurrentIndex();
LLGameControl::Options& options = getSelectedDeviceOptions();
if (output >= 0 && output < (S32)LLGameControl::NUM_BUTTONS)
{
options.getButtonMap()[button] = (U8)output;
LLGameControl::setDeviceOptions(mSelectedDeviceGUID, options);
}
// Both output cells (icon + description) are re-rendered from the actual
// map by populateButtonChannelsCells() below.
}
// The branches above set the cell to its committed value; keep the
// restore-on-close value in sync so clearSelectionState() doesn't revert
// the cell back to its pre-edit text.
sSelectedCellValue = sSelectedCell->getValue();
sSelectedGrid->deselectAllItems();
clearSelectionState();
// Rebuild the action tables so a newly bound (or unbound) button action is
// re-rendered with the correct cell font -- text label vs SecondLife-game-control
// glyph.
if (action_grid_edited)
{
populateActionMappings();
}
// Refresh the axis-/button-channels output cells so the committed output's text
// label and SecondLife-game-control glyph are both re-rendered from the device map.
if (axis_channels_edited)
{
populateAxisChannelsCells();
}
if (button_channels_edited)
{
populateButtonChannelsCells();
}
}
// Returns true if a cell is currently selected and waiting for input channel assignment.
// Used to determine whether to capture live controller input.
bool LLPanelPreferenceGameControl::isWaitingForInputChannel()
{
return sSelectedCell != nullptr;
}
// Static method called in the mainloop when an Action Mappings input selector is open.
// Refreshes the canonical controller state, and if the user is pressing a button or
// tilting an axis that matches the input_type of selector open (button vs. axis), assigns
// it to the selected cell as if the user had picked it from the dropdown.
void LLPanelPreferenceGameControl::applyGameControlInput()
{
if (!sGameControlPanel || !sSelectedCombobox)
return;
bool wants_axis = (sSelectedGrid == sGameControlPanel->mActionMappingsAxes);
bool wants_button = (sSelectedGrid == sGameControlPanel->mActionMappingsButtons);
if (!wants_axis && !wants_button)
return;
// Recompute the canonical controller state from the live device input without
// triggering the normal "send to server" bookkeeping (see computeFinalStateAndCheckForChanges()),
// since input used to configure a mapping should not drive the avatar or the network.
LLGameControl::computeFinalState();
LLGameControl::InputChannel channel = LLGameControl::getActiveInputChannel();
if (wants_axis && !channel.isAxis())
return;
if (wants_button && !channel.isButton())
return;
std::string value = channel.getRemoteName();
if (!sSelectedCombobox->setSelectedByValue(value, true))
{
// The action-mapping axis selector has no entries for the individual
// trigger axes: it collapses them into one virtual "Triggers" item
// (see AXIS_TRIGGERS in panel_preferences_game_control.xml).
if (value != "AXIS_LEFT_TRIGGER" && value != "AXIS_RIGHT_TRIGGER")
return;
if (!sSelectedCombobox->setSelectedByValue(std::string("AXIS_TRIGGERS"), true))
return;
}
sGameControlPanel->onCommitInputChannel(sSelectedCombobox);
}
// Handles selection in the axis-channels table (invert, deadzone, offset, output).
// Syncs the invert flag immediately; shows the numeric editor for deadzone/offset,
// or the axis selector popup for the Output column.
void LLPanelPreferenceGameControl::onAxisChannelsSelect()
{
clearSelectionState();
// Deselect everything in the other scroll_list
mButtonChannels->deselectAllItems(true);
if (LLScrollListItem* row = mAxisChannels->getFirstSelected())
{
// Only the Output columns (icon + description) are editable; initCombobox
// ignores the fixed Input columns. Axis tuning (invert/offset/dead zone)
// now lives on the Device State tab.
initCombobox(row, mAxisChannels);
}
}
// Handles selection in the (now editable) axis-state table on the Device State
// tab. Syncs the Invert checkbox immediately and shows the numeric editor for the
// Offset/Dead Zone columns. Edits target the state tab's selected device.
void LLPanelPreferenceGameControl::onAxisStateSelect()
{
clearSelectionState();
// Deselect everything in the other scroll_list
mButtonState->deselectAllItems(true);
auto options_it = mDeviceOptions.find(mStateSelectedDeviceGUID);
if (options_it == mDeviceOptions.end())
{
return;
}
LLGameControl::Options& deviceOptions = options_it->second.options;
if (LLScrollListItem* row = mAxisState->getFirstSelected())
{
S32 row_index = mAxisState->getItemIndex(row);
{
// Always sync invert checkbox - clicking the checkbox selects the row
// but doesn't automatically update the underlying option.
S32 invert_checkbox_column = axisStateColumn("invert");
bool invert = row->getColumn(invert_checkbox_column)->getValue().asBoolean();
S32 multiplier = invert ? -1 : 1;
if (multiplier != deviceOptions.getAxisOptions()[row_index].mMultiplier)
{
deviceOptions.getAxisOptions()[row_index].mMultiplier = multiplier;
if (row_index == LLGameControl::AXIS_LEFT_TRIGGER
|| row_index == LLGameControl::AXIS_RIGHT_TRIGGER)
{
// The two trigger axes act as one bidirectional axis under the hood,
// so their inversion must stay in sync -- update the sibling axis's
// option and reflect the new state in its checkbox cell.
S32 other_row_index = row_index == LLGameControl::AXIS_LEFT_TRIGGER
? LLGameControl::AXIS_RIGHT_TRIGGER
: LLGameControl::AXIS_LEFT_TRIGGER;
deviceOptions.getAxisOptions()[other_row_index].mMultiplier = multiplier;
if (LLScrollListItem* other_row = mAxisState->getItemByIndex(other_row_index))
{
other_row->getColumn(invert_checkbox_column)->setValue(invert);
}
}
LLGameControl::setDeviceOptions(mStateSelectedDeviceGUID, deviceOptions);
}
}
S32 offset_col = axisStateColumn("offset");
S32 dead_zone_col = axisStateColumn("dead_zone");
S32 column_index = row->getSelectedCell();
if (column_index == offset_col || column_index == dead_zone_col)
{
fitInRect(mNumericValueEditor, mAxisState, row_index, column_index);
if (column_index == dead_zone_col) // Dead Zone
{
mNumericValueEditor->setMinValue(0);
mNumericValueEditor->setMaxValue(LLGameControl::MAX_AXIS_DEAD_ZONE);
mNumericValueEditor->setValue(deviceOptions.getAxisOptions()[row_index].mDeadZone);
}
else // Offset
{
mNumericValueEditor->setMinValue(-LLGameControl::MAX_AXIS_OFFSET);
mNumericValueEditor->setMaxValue(LLGameControl::MAX_AXIS_OFFSET);
mNumericValueEditor->setValue(deviceOptions.getAxisOptions()[row_index].mOffset);
}
mNumericValueEditor->setVisible(true);
}
}
}
// Handles selection in the button-channels table. Only the Output columns (icon +
// description) use a popup selector; the Input columns are fixed (the physical button).
void LLPanelPreferenceGameControl::onButtonChannelsSelect()
{
clearSelectionState();
// Deselect everything in the other scroll_list
mAxisChannels->deselectAllItems(true);
if (LLScrollListItem* row = mButtonChannels->getFirstSelected())
{
initCombobox(row, mButtonChannels);
}
}
// Called when user commits a numeric value (offset or dead zone) in the spin
// control over the Device State tab's axis table. Validates and clamps the value,
// then updates both the UI and the state device's options.
void LLPanelPreferenceGameControl::onCommitNumericValue()
{
auto options_it = mDeviceOptions.find(mStateSelectedDeviceGUID);
if (options_it == mDeviceOptions.end())
{
return;
}
LLGameControl::Options& deviceOptions = options_it->second.options;
if (LLScrollListItem* row = mAxisState->getFirstSelected())
{
S32 value = mNumericValueEditor->getValue().asInteger();
S32 row_index = mAxisState->getItemIndex(row);
S32 offset_col = axisStateColumn("offset");
S32 dead_zone_col = axisStateColumn("dead_zone");
S32 column_index = row->getSelectedCell();
llassert(column_index == offset_col || column_index == dead_zone_col);
if (column_index != offset_col && column_index != dead_zone_col)
return;
if (column_index == dead_zone_col) // Dead Zone
{
value = std::clamp<S32>(value, 0, LLGameControl::MAX_AXIS_DEAD_ZONE);
deviceOptions.getAxisOptions()[row_index].mDeadZone = (U16)value;
}
else // Offset
{
value = std::clamp<S32>(value, -LLGameControl::MAX_AXIS_OFFSET, LLGameControl::MAX_AXIS_OFFSET);
deviceOptions.getAxisOptions()[row_index].mOffset = (S16)value;
}
setNumericLabel(row->getColumn(column_index), value);
LLGameControl::setDeviceOptions(mStateSelectedDeviceGUID, deviceOptions);
}
}
// Initializes all UI controls and sets up callbacks.
// Called once when the panel is first built from XML.
bool LLPanelPreferenceGameControl::postBuild()
{
// Send-to-server checkbox (on the Server Data sub-tab)
mCheckGameControlToServer = getChild<LLCheckBoxCtrl>("game_control_to_server");
mCheckGameControlToServer->setCommitCallback([this](LLUICtrl*, const LLSD&)
{
LLGameControl::setSendToServer(mCheckGameControlToServer->getValue());
});
getChild<LLTabContainer>("game_control_tabs")->setCommitCallback([this](LLUICtrl*, const LLSD&) { clearSelectionState(); });
mTabActions = getChild<LLPanel>("tab_action_mappings");
mTabDevices = getChild<LLPanel>("tab_device_mappings");
// Actions tab (global, per-mode). The mode items (Avatar/FlyCam/Captive) are
// defined in the XML; only one mode is visible at a time, with a single
// Enabled checkbox that tracks whichever mode is currently selected.
mActionMode = getChild<LLComboBox>("action_mode");
mActionMode->setCommitCallback([this](LLUICtrl*, const LLSD&) { onActionModeChanged(); });
mCheckActionModeEnabled = getChild<LLCheckBoxCtrl>("action_mode_enabled");
mCheckActionModeEnabled->setCommitCallback([this](LLUICtrl*, const LLSD&)
{ onModeEnabledToggled(mCheckActionModeEnabled->getValue()); });
mCheckFlycamAllowRoll = getChild<LLCheckBoxCtrl>("flycam_allow_roll");
mCheckFlycamAllowRoll->setCommitCallback([this](LLUICtrl*, const LLSD&)
{
clearSelectionState();
LLGameControl::setFlycamRollAllowed(mCheckFlycamAllowRoll->getValue());
populateActionMappings();
});
// Shared by FlyCam, Mouselook, and Avatar: applies to whichever mode is being edited.
mSliderSpeedFactor = getChild<LLSliderCtrl>("speed_factor");
mSliderSpeedFactor->setCommitCallback([this](LLUICtrl*, const LLSD&)
{ LLGameControl::setSpeedFactor(currentEditMode(), (F32)mSliderSpeedFactor->getValue().asReal()); });
mSpeedSlowLabel = getChild<LLTextBox>("speed_slow_label");
mSpeedFastLabel = getChild<LLTextBox>("speed_fast_label");
mCheckFlycamCrosshair = getChild<LLCheckBoxCtrl>("flycam_crosshair");
mCheckFlycamCrosshair->setCommitCallback([this](LLUICtrl*, const LLSD&)
{ LLGameControl::setFlycamCrosshairEnabled(mCheckFlycamCrosshair->getValue()); });
mCheckFlycamHideCursor = getChild<LLCheckBoxCtrl>("flycam_hide_cursor");
mCheckFlycamHideCursor->setCommitCallback([this](LLUICtrl*, const LLSD&)
{ LLGameControl::setFlycamHideCursorEnabled(mCheckFlycamHideCursor->getValue()); });
mRestoreActionsDefaults = getChild<LLButton>("restore_actions_defaults");
mRestoreActionsDefaults->setCommitCallback([this](LLUICtrl*, const LLSD&) { onResetActionsToDefaults(); });
mCurrentModeIndicator = getChild<LLTextBox>("current_mode_indicator");
mActionMappingsAxes = getChild<LLScrollListCtrl>("action_mappings_axes");
mActionMappingsAxes->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onGridSelect(ctrl); });
mActionMappingsButtons = getChild<LLScrollListCtrl>("action_mappings_buttons");
mActionMappingsButtons->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onGridSelect(ctrl); });
// Inputs tab (per-device)
mNoDeviceMessage = getChild<LLTextBox>("nodevice_message");
mDevicePrompt = getChild<LLTextBox>("device_prompt");
mDeviceList = getChild<LLComboBox>("device_list");
mCheckShowAllDevices = getChild<LLCheckBoxCtrl>("show_all_known_devices");
mPanelDeviceSettings = getChild<LLPanel>("device_settings");
mCheckShowAllDevices->setCommitCallback([this](LLUICtrl*, const LLSD&) { populateDeviceTitle(); });
mDeviceList->setCommitCallback([this](LLUICtrl*, const LLSD& value) { populateDeviceSettings(value); });
mRestoreDeviceDefaults = getChild<LLButton>("restore_device_defaults");
mRestoreDeviceDefaults->setCommitCallback([this](LLUICtrl*, const LLSD&) { onResetDeviceToDefaults(); });
mAxisChannels = getChild<LLScrollListCtrl>("axis_channels");
mAxisChannels->setCommitCallback([this](LLUICtrl*, const LLSD&) { onAxisChannelsSelect(); });
mButtonChannels = getChild<LLScrollListCtrl>("button_channels");
mButtonChannels->setCommitCallback([this](LLUICtrl*, const LLSD&) { onButtonChannelsSelect(); });
// Outputs tab (per device)
mTabDeviceState = getChild<LLPanel>("tab_device_options");
mStateNoDeviceMessage = getChild<LLTextBox>("state_nodevice_message");
mStateDevicePrompt = getChild<LLTextBox>("state_device_prompt");
mStateRemapNote = getChild<LLTextBox>("state_remap_note");
mStateDeviceList = getChild<LLComboBox>("options_device_list");
mPanelDeviceState = getChild<LLPanel>("device_state_settings");
mStateDeviceList->setCommitCallback([this](LLUICtrl*, const LLSD& value)
{
mStateSelectedDeviceGUID = value.asString();
clearSelectionState();
mCalibrating = false; // switching devices invalidates any in-progress calibration
populateAxisStateOptionCells(); // refresh invert/offset/dead-zone for the new device
});
mRestoreDeviceOptionsDefaults = getChild<LLButton>("restore_device_options_defaults");
mRestoreDeviceOptionsDefaults->setCommitCallback([this](LLUICtrl*, const LLSD&) { onResetDeviceOptionsToDefaults(); });
mAutoCalibrate = getChild<LLButton>("auto_calibrate");
mAutoCalibrate->setCommitCallback([this](LLUICtrl*, const LLSD&) { onAutoCalibrate(); });
mAxisState = getChild<LLScrollListCtrl>("axis_state");
// The axis-state table is now editable: its Invert/Offset/Dead Zone columns
// tune the state device's axis options while its Raw/Adjusted columns update live.
mAxisState->setCommitCallback([this](LLUICtrl*, const LLSD&) { onAxisStateSelect(); });
mButtonState = getChild<LLScrollListCtrl>("button_state");
// Data Output tab (live, read-only view of the last outgoing GameControlInput)
mTabDataOutput = getChild<LLPanel>("tab_server_data");
mDataOutput = getChild<LLScrollListCtrl>("data_output");
// Spin control for editing deadzone/offset values inline
mNumericValueEditor = getChild<LLSpinCtrl>("numeric_value_editor");
mNumericValueEditor->setCommitCallback([this](LLUICtrl*, const LLSD&) { onCommitNumericValue(); });
// Action selectors: provide the "Action" column rows, chosen by edit mode.
mAnalogActionSelector = getChild<LLComboBox>("analog_action_selector");
mBinaryActionSelector = getChild<LLComboBox>("binary_action_selector");
mMouselookAnalogActionSelector = getChild<LLComboBox>("mouselook_analog_action_selector");
mMouselookBinaryActionSelector = getChild<LLComboBox>("mouselook_binary_action_selector");
mFlycamAnalogActionSelector = getChild<LLComboBox>("flycam_analog_action_selector");
mFlycamBinaryActionSelector = getChild<LLComboBox>("flycam_binary_action_selector");
mCaptiveBinaryActionSelector = getChild<LLComboBox>("sit_binary_action_selector");
mCursorAnalogActionSelector = getChild<LLComboBox>("cursor_analog_action_selector");
mCursorBinaryActionSelector = getChild<LLComboBox>("cursor_binary_action_selector");
// Canonical input selectors, shown inline when editing an Input/Output cell.
// The Actions tab binds an axis action to a canonical input (sticks or the
// "Triggers left/right" pair) via mAxisInputSelector; the Devices tab maps a
// physical axis to an individual canonical axis via mAxisOutputSelector.
mAxisInputSelector = getChild<LLComboBox>("axis_input_selector");
mAxisInputSelector->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onCommitInputChannel(ctrl); });
mAxisOutputSelector = getChild<LLComboBox>("axis_output_selector");
mAxisOutputSelector->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onCommitInputChannel(ctrl); });
// Glyph counterpart of mAxisOutputSelector, shown when editing the
// SecondLife-game-control "output" (icon) column of the axis-channels table.
// Same values, rendered as SecondLife-game-control glyphs, so build it from
// the text selector's values.
mAxisOutputGlyphSelector = getChild<LLComboBox>("axis_output_glyph_selector");
mAxisOutputGlyphSelector->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onCommitInputChannel(ctrl); });
buildInputGlyphSelector(mAxisOutputSelector, mAxisOutputGlyphSelector);
mButtonInputSelector = getChild<LLComboBox>("button_input_selector");
mButtonInputSelector->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onCommitInputChannel(ctrl); });
// Glyph selectors shown when editing the SecondLife-game-control
// "Axis"/"Button" column. Their items mirror the text selectors' values, so
// build them from those.
mAxisInputGlyphSelector = getChild<LLComboBox>("axis_input_glyph_selector");
mAxisInputGlyphSelector->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onCommitInputChannel(ctrl); });
buildInputGlyphSelector(mAxisInputSelector, mAxisInputGlyphSelector);
mButtonInputGlyphSelector = getChild<LLComboBox>("button_input_glyph_selector");
mButtonInputGlyphSelector->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onCommitInputChannel(ctrl); });
buildInputGlyphSelector(mButtonInputSelector, mButtonInputGlyphSelector);
// The device tables' rows are static (one per physical axis/button); their
// cells are filled per-device. The action table is mode-specific and built
// in populateActionMappings() on open / mode change.
populateAxisChannelsRows();
populateButtonChannelsRows();
// The Device State tab's rows are static (one per physical axis/button); their
// Value cells are refreshed every frame in draw() from the selected device.
populateAxisStateRows();
populateButtonStateRows();
// The Data Output tab's rows are static (AXIS_0..5, a blank separator, then
// BUTTON_0..31); their Value cells are refreshed whenever a fresh
// GameControlInput message is sent (see updateDataOutput()).
populateDataOutputRows();
// Workaround for the common bug:
// LLScrollListCtrl with draw_heading="true" initially has incorrect mTop (17 px higher).
// Each scroll list fills a dedicated wrapper panel, so the corrected top is its
// parent's height - 1.
auto fixHeadingTop = [](LLScrollListCtrl* grid)
{
LLRect rect = grid->getRect();
rect.mTop = grid->getParent()->getRect().getHeight() - 1;
grid->setRect(rect);
grid->updateLayout();
};
fixHeadingTop(mActionMappingsAxes);
fixHeadingTop(mActionMappingsButtons);
fixHeadingTop(mAxisChannels);
fixHeadingTop(mButtonChannels);
fixHeadingTop(mAxisState);
fixHeadingTop(mButtonState);
fixHeadingTop(mDataOutput);
return true;
}
// Called when the preferences floater is opened.
// Loads current LLGameControl state into UI controls and refreshes all tables.
void LLPanelPreferenceGameControl::onOpen(const LLSD& key)
{
// Sync checkbox with current LLGameControl state
mCheckGameControlToServer->setValue(LLGameControl::sendToServer());
clearSelectionState();
// Default to editing whichever mode is currently active in-world, so the
// panel opens already showing the mappings the user is presently driving
// with; fall back to Avatar (the first action_mode item) when no mode is
// active (CONTROL_MODE_NONE, e.g. not yet logged in).
LLGameControl::AgentControlMode live_mode = LLGameControl::getAgentControlMode();
if (live_mode == LLGameControl::CONTROL_MODE_NONE || !mActionMode->selectByValue((S32)live_mode))
{
mActionMode->selectFirstItem();
}
populateActionMappings();
// Refresh device list and settings
updateDeviceListInternal();
mCheckGameControlToServer->setEnabled(true);
mActionMappingsAxes->setEnabled(true);
mActionMappingsButtons->setEnabled(true);
mAxisChannels->setEnabled(true);
mButtonChannels->setEnabled(true);
mDeviceList->setEnabled(true);
mStateDeviceList->setEnabled(true);
// Apply the current mode's enable flag (must run after the blanket enables
// above): syncs the checkbox and locks the tables if the mode is disabled.
updateActionModeEnabledUI();
// Clear original settings - will be populated on first saveSettings() call
mOrigSettings = LLSD::emptyMap();
}
static LLRect computeCellScreenRect(LLScrollListCtrl* grid, LLView* relative_to, S32 row_index, S32 col_index)
{
LLRect rect(grid->getCellRect(row_index, col_index));
LLView* parent = grid->getParent();
while (parent && parent != relative_to)
{
rect.translate(parent->getRect().mLeft, parent->getRect().mBottom);
parent = parent->getParent();
}
return rect;
}
// Per-frame refresh of the Device State tab. Only touches the tables while that
// tab is actually the visible one, so it costs nothing on the other tabs.
void LLPanelPreferenceGameControl::draw()
{
updateCurrentModeIndicator();
if (mTabDeviceState && mTabDeviceState->getVisible())
{
populateDeviceStateValues();
if (mCalibrating)
{
updateAutoCalibration();
}
}
if (sSelectedCombobox && sSelectedGrid && sSelectedRow >= 0 && sSelectedCol >= 0)
{
LLRect current_rect = computeCellScreenRect(sSelectedGrid, sSelectedCombobox->getParent(), sSelectedRow, sSelectedCol);
if (current_rect != sSelectedOpenRect)
{
clearSelectionState();
}
}
LLPanelPreference::draw();
}
// Called when user clicks OK. Clears selections and prepares for onClose()
void LLPanelPreferenceGameControl::apply()
{
LLPanelPreference::apply();
clearSelectionState();
// Clear mOrigSettings to prevent cancel() from reverting changes.
// Note: cancel() is called in onClose() even after OK, so we must clear this.
mOrigSettings = LLSD::emptyMap();
// Note: any settings changes have already been written to global-settings
// via the LLControlVariables assigned to the UI elements. LLGameControl has
// already loaded from global-settings in saveSettings().
}
// Called when user clicks Cancel. Restores all settings to their original values.
void LLPanelPreferenceGameControl::cancel(const std::vector<std::string> settings_to_skip)
{
LLPanelPreference::cancel(settings_to_skip);
clearSelectionState();
// Skip restore if original settings were cleared in apply().
if (mOrigSettings.isEmpty())
{
return;
}
// Restore in-memory state from the snapshot, then push back to gSavedSettings
// via the bulk save path so persistence stays consistent.
LLGameControl::applySettingsFromLLSD(mOrigSettings);
LLGameControl::saveToSettings();
}
// Rebuilds the internal device options map from LLGameControl state.
// Merges saved options with currently connected devices.
void LLPanelPreferenceGameControl::updateDeviceListInternal()
{
mDeviceOptions.clear();
// Load saved device options from settings. Each device's saved Config is
// parsed independently -- one entry with a corrupted/hand-edited Config
// (loadFromString() returns false) falls back to default Options rather
// than being dropped or affecting any other device's entry in this loop.
for (const auto& [guid, options] : LLGameControl::getDeviceOptions())
{
DeviceOptions deviceOptions = { LLStringUtil::null, options, LLGameControl::Options() };
if (!deviceOptions.options.loadFromString(deviceOptions.name, deviceOptions.settings))
{
LL_WARNS("SDL3") << "Discarding invalid saved device options for '" << guid
<< "'; using defaults instead" << LL_ENDL;
}
if (deviceOptions.name.empty())
{
// Parsing failed before reaching the saved name, or the saved
// Config simply never recorded one -- fall back to the guid so
// the device list never shows a blank title.
deviceOptions.name = guid;
}
mDeviceOptions.emplace(guid, deviceOptions);
}
// Add currently connected devices that don't have saved settings yet
for (const auto& device : LLGameControl::getDevices())
{
if (mDeviceOptions.find(device.getGUID()) == mDeviceOptions.end())
{
mDeviceOptions[device.getGUID()] = { device.getName(), device.saveOptionsToString(true), device.getOptions() };
}
}
mCheckShowAllDevices->setValue(false);
populateDeviceTitle();
// The Device State tab tracks the live set of connected devices too.
updateDeviceStateList();
}
// Updates the device selection UI based on available devices.
// Shows single device name, dropdown list, or "no device" message as appropriate.
void LLPanelPreferenceGameControl::populateDeviceTitle()
{
mSelectedDeviceGUID.clear();
// "Show all devices" includes saved settings for disconnected devices
bool showAllDevices = mCheckShowAllDevices->getValue().asBoolean();
std::size_t deviceCount = showAllDevices ? mDeviceOptions.size() : LLGameControl::getDevices().size();
mNoDeviceMessage->setVisible(!deviceCount);
mDevicePrompt->setVisible(deviceCount);
mDeviceList->setVisible(deviceCount); // always use the combo box, even for one device
mPanelDeviceSettings->setVisible(deviceCount);
mRestoreDeviceDefaults->setVisible(deviceCount);
if (!deviceCount)
{
return;
}
auto makeTitle = [](const std::string& guid, const std::string& name) -> std::string
{
return guid + ", " + name;
};
auto makeListItem = [](const std::string& guid, const std::string& title)
{
return LLSD().with("value", guid).with("columns", LLSD().with("label", title));
};
mDeviceList->clear();
mDeviceList->clearRows();
if (showAllDevices)
{
for (const auto& [guid, device] : mDeviceOptions)
{
mDeviceList->addElement(makeListItem(guid, makeTitle(guid, device.name)));
}
}
else
{
for (const LLGameControl::Device& device : LLGameControl::getDevices())
{
mDeviceList->addElement(makeListItem(device.getGUID(), makeTitle(device.getGUID(), device.getName())));
}
}
mDeviceList->selectNthItem(0);
populateDeviceSettings(mDeviceList->getValue());
}
// Loads settings for the specified device into the device-channel tables.
void LLPanelPreferenceGameControl::populateDeviceSettings(const std::string& guid)
{
mSelectedDeviceGUID = guid;
llassert_always(mDeviceOptions.find(guid) != mDeviceOptions.end());
// Device tables are per-device (mode-independent).
populateAxisChannelsCells();
populateButtonChannelsCells();
}
// The mode string ("When moving Avatar"/"When moving FlyCam"/"When sitting"/
// "When in mouselook") currently selected for editing.
std::string LLPanelPreferenceGameControl::currentEditMode()
{
S32 ordinal = mActionMode->getValue().asInteger();
return LLGameControl::getModeName((LLGameControl::AgentControlMode)ordinal);
}
// True for the FlyCam Roll actions, and for Unroll (which only undoes roll),
// while "Allow Roll" is unchecked.
bool LLPanelPreferenceGameControl::isFlycamRollDisallowed(const std::string& mode, const std::string& action)
{
return mode == "FlyCam" && !LLGameControl::isFlycamRollAllowed()
&& (action == "Roll CCW/CW" || action == "Roll CW" || action == "Roll CCW" || action == "Unroll");
}
// Picks the action selector supplying the "Action" rows for a block + mode.
LLComboBox* LLPanelPreferenceGameControl::actionSelectorForMode(bool axis, const std::string& mode) const
{
bool flycam = (mode == "FlyCam");
bool mouselook = (mode == "Mouselook");
bool cursor = (mode == "Cursor");
// Avatar and Captive share the same axis actions (see llgamecontrol.cpp);
// Mouselook and Cursor each have their own axis-action selector too, since
// Cursor swaps in "Mouse left/right"/"Mouse up/down" for Strafe/Advance.
// Each mode has its own button-action selector so their button sets can
// diverge (e.g. Captive swaps in "Stand" for "Jump", Cursor adds "Mouse
// click left/right").
if (axis)
return flycam ? mFlycamAnalogActionSelector
: mouselook ? mMouselookAnalogActionSelector
: cursor ? mCursorAnalogActionSelector
: mAnalogActionSelector;
if (mouselook)
return mMouselookBinaryActionSelector;
if (flycam)
return mFlycamBinaryActionSelector;
if (cursor)
return mCursorBinaryActionSelector;
return (mode == "Captive") ? mCaptiveBinaryActionSelector : mBinaryActionSelector;
}
// Rebuilds the two Actions tables for the current mode: axis actions in the top
// table, button actions in the bottom table. Each row stores {input_type, action} as its
// value and shows the action label + its currently-mapped input.
void LLPanelPreferenceGameControl::populateActionMappings()
{
mActionMappingsAxes->clearRows();
mActionMappingsButtons->clearRows();
std::string mode = currentEditMode();
auto addBlock = [&](LLScrollListCtrl* grid, const std::string& input_type,
const LLComboBox* action_selector, const LLComboBox* input_selector)
{
LLSD mapping = LLGameControl::getModeMapping(mode, input_type);
// Axes-only Invert column: -1 for the Buttons block (no such column there).
// The selector's last item is the "None" value (e.g. AXIS_NONE), used below
// to tell an unmapped row from a mapped one.
S32 invert_col = (input_type == MODE_INPUT_TYPE_AXES) ? grid->getColumn("invert")->mIndex : -1;
std::string none_value = input_selector->getAllData().back()->getValue().asString();
for (LLScrollListItem* item : action_selector->getAllData())
{
std::string action = item->getValue().asString();
if (action == NONE_LABEL) // skip the "None" sentinel; not an action row
continue;
// Current input bound to this action, and the SecondLife-game-control
// glyph to show instead of the text label, if this input has one.
std::string input_value = mapping.has(action) ? mapping[action].asString() : LLStringUtil::null;
std::string glyph = promptFontGlyph(input_value);
// Roll and Unroll stay mapped (and shown) while disallowed, but greyed
// out to match their lack of effect on flycam behavior until re-enabled.
// The row itself stays enabled/clickable so an existing mapping can
// still be cleared to None; onCommitInputChannel() refuses to set
// anything else while disallowed.
bool roll_disabled = isFlycamRollDisallowed(mode, action);
LLScrollListItem::Params row_params;
for (S32 c = 0; c < grid->getNumColumns(); ++c)
{
LLScrollListCell::Params cell_params;
const std::string& col_name = grid->getColumn(c)->mName;
cell_params.column = col_name;
// The icon column (1) renders the controller glyph in
// SecondLife-game-control font when this input has one; the action
// (0) and description (2) columns match the "Controls" preferences
// panel font.
cell_params.font = (c == 1 && !glyph.empty())
? promptFontForCell()
: LLFontGL::getFontSansSerif();
if (c == invert_col)
{
cell_params.type = "checkbox";
cell_params.font_halign = "left"; // left-align the checkbox within the cell
}
row_params.columns.add(cell_params);
}
// Row value carries the block input_type and action key (used on commit/lookup).
LLSD row_value;
row_value["input_type"] = input_type;
row_value["action"] = action;
row_params.value = row_value;
LLScrollListItem* row = grid->addRow(row_params);
row->getColumn(0)->setValue(item->getColumn(0)->getValue()); // action label
// Columns 1 (icon) and 2 (description) both derive from input_value, so
// they always refer to the same controller input.
row->getColumn(1)->setValue(glyph.empty() ? LLSD() : LLSD(glyph));
row->getColumn(2)->setValue(blankIfNone(inputLabel(input_selector, input_value)));
if (invert_col >= 0)
{
// Disabled (and left at its last value) whenever no axis is mapped,
// since there is nothing to invert.
bool mapped = !input_value.empty() && input_value != none_value;
row->getColumn(invert_col)->setValue(LLGameControl::getAxisInvert(mode, action));
row->getColumn(invert_col)->setEnabled(mapped && !roll_disabled);
}
if (roll_disabled)
{
// Grey the text to signal "unavailable" without disabling the row,
// which would also block clicking an existing mapping to clear it.
LLColor4 disabled_color = LLUIColorTable::instance().getColor("LabelDisabledColor");
row->getColumn(0)->setColor(disabled_color);
row->getColumn(1)->setColor(disabled_color);
row->getColumn(2)->setColor(disabled_color);
}
}
};
addBlock(mActionMappingsAxes, MODE_INPUT_TYPE_AXES, actionSelectorForMode(true, mode), mAxisInputSelector);
addBlock(mActionMappingsButtons, MODE_INPUT_TYPE_BUTTONS, actionSelectorForMode(false, mode), mButtonInputSelector);
}
// Handles a change in the edit-mode selector: rebuild the action table.
void LLPanelPreferenceGameControl::onActionModeChanged()
{
clearSelectionState();
populateActionMappings();
updateActionModeEnabledUI();
}
// Toggle whether game-control input is converted to the currently-selected
// mode's actions.
void LLPanelPreferenceGameControl::onModeEnabledToggled(bool enabled)
{
clearSelectionState();
LLGameControl::setModeEnabled(currentEditMode(), enabled);
updateActionModeEnabledUI();
}
// Sync the Enabled checkbox with the selected mode's stored flag (keeps it
// correct after apply/cancel/reset and after switching modes) and lock/unlock
// the action tables (and Restore Defaults) so a disabled mode's mappings can't change.
void LLPanelPreferenceGameControl::updateActionModeEnabledUI()
{
std::string mode = currentEditMode();
bool enabled = LLGameControl::isModeEnabled(mode);
mCheckActionModeEnabled->set(enabled);
// A disabled mode's tables grey out; onGridSelect() also refuses edits on a
// disabled table, so this both signals and enforces the lock.
mActionMappingsAxes->setEnabled(enabled);
mActionMappingsButtons->setEnabled(enabled);
mRestoreActionsDefaults->setEnabled(enabled);
// Roll, the crosshair, and cursor hiding are only meaningful for FlyCam, and the speed
// slider for FlyCam, Mouselook, and Avatar: hide them for every other mode.
bool is_flycam = (mode == "FlyCam");
bool has_speed_factor = is_flycam || (mode == "Mouselook") || (mode == "Avatar");
mCheckFlycamAllowRoll->setVisible(is_flycam);
mCheckFlycamCrosshair->setVisible(is_flycam);
mCheckFlycamHideCursor->setVisible(is_flycam);
mSliderSpeedFactor->setVisible(has_speed_factor);
mSpeedSlowLabel->setVisible(has_speed_factor);
mSpeedFastLabel->setVisible(has_speed_factor);
if (is_flycam)
{
mCheckFlycamAllowRoll->set(LLGameControl::isFlycamRollAllowed());
mCheckFlycamAllowRoll->setEnabled(enabled);
mCheckFlycamCrosshair->set(LLGameControl::isFlycamCrosshairEnabled());
mCheckFlycamCrosshair->setEnabled(enabled);
mCheckFlycamHideCursor->set(LLGameControl::isFlycamHideCursorEnabled());
mCheckFlycamHideCursor->setEnabled(enabled);
}
if (has_speed_factor)
{
mSliderSpeedFactor->setValue(LLGameControl::getSpeedFactor(mode));
mSliderSpeedFactor->setEnabled(enabled);
}
}
// Shows the live AgentControlMode independent of the mode being edited (mActionMode)
void LLPanelPreferenceGameControl::updateCurrentModeIndicator()
{
LLGameControl::AgentControlMode mode = LLGameControl::getAgentControlMode();
if (mode == mLastDrawnControlMode)
{
return; // no change, skip the string work
}
mLastDrawnControlMode = mode;
if (mCurrentModeIndicator)
{
static const std::map<LLGameControl::AgentControlMode, std::string> sDisplayNames = {
{ LLGameControl::CONTROL_MODE_AVATAR, "Moving Avatar" },
{ LLGameControl::CONTROL_MODE_MOUSELOOK, "Mouselook" },
{ LLGameControl::CONTROL_MODE_FLYCAM, "FlyCam" },
{ LLGameControl::CONTROL_MODE_CAPTIVE, "Sitting" },
{ LLGameControl::CONTROL_MODE_CURSOR, "Mouse Cursor" },
};
auto it = sDisplayNames.find(mode);
std::string name = (it != sDisplayNames.end()) ? it->second : "None";
mCurrentModeIndicator->setText(std::string("Active mode: ") + name);
}
}
// Enforces that an input drives at most one action within a mode's block. When a
// new (non-None) input is assigned to an action, any *other* action in the same
// mode+input_type mapping that currently uses that same input is reset to "None"
// (its Input cell blanked and its mapping set to the selector's None value).
void LLPanelPreferenceGameControl::removeDuplicateActionInput(const std::string& mode,
const std::string& input_type, const std::string& keep_action, const std::string& input_value,
const LLComboBox* input_selector)
{
// The selector's last item is the "None" value (e.g. AXIS_NONE / BUTTON_NONE).
std::string none_value = input_selector->getAllData().back()->getValue().asString();
LLSD mapping = LLGameControl::getModeMapping(mode, input_type);
LLScrollListCtrl* grid = (input_type == MODE_INPUT_TYPE_AXES) ? mActionMappingsAxes : mActionMappingsButtons;
for (LLScrollListItem* row : grid->getAllData())
{
LLSD row_value = row->getValue();
if (!row_value.isMap())
continue;
std::string action = row_value["action"].asString();
if (action == keep_action)
continue;
if (mapping.has(action) && mapping[action].asString() == input_value)
{
LLGameControl::updateModeMapping(mode, input_type, action, none_value);
row->getColumn(1)->setValue(LLSD()); // blank the Input cell
}
}
}
// Creates one row per physical axis in the axis-channels table. Columns:
// input(glyph) | input_description | output_description | output(glyph). The two
// Input columns are the fixed physical axis (icon + text); the two Output columns
// are the canonical axis it maps to (icon + text), edited via popup selectors.
// Axis tuning (invert/offset/dead zone) lives on the Device State tab.
void LLPanelPreferenceGameControl::populateAxisChannelsRows()
{
mAxisChannels->clearRows();
// The physical controller axes are the output selector's first NUM_AXES items,
// in order (item N == canonical axis N).
std::vector<LLScrollListItem*> items = mAxisOutputSelector->getAllData();
S32 input_glyph_col = mAxisChannels->getColumn("input")->mIndex;
S32 input_desc_col = mAxisChannels->getColumn("input_description")->mIndex;
LLScrollListItem::Params row_params;
LLScrollListCell::Params cell_params;
for (S32 i = 0; i < (S32)(mAxisChannels->getNumColumns()); ++i)
{
const std::string& name = mAxisChannels->getColumn(i)->mName;
// The icon columns ("input"/"output") render the controller glyph in
// SecondLife-game-control; the description columns match the "Controls"
// panel font.
cell_params.font = (name == "input" || name == "output")
? promptFontForCell()
: LLFontGL::getFontSansSerif();
cell_params.column = name;
row_params.columns.add(cell_params);
}
for (size_t i = 0; i < LLGameControl::NUM_AXES; ++i)
{
LLScrollListItem* row = mAxisChannels->addRow(row_params);
// The physical axis is fixed: fill both its text label and its glyph.
std::string glyph = promptFontGlyph(items[i]->getValue().asString());
row->getColumn(input_desc_col)->setValue(items[i]->getColumn(0)->getValue());
row->getColumn(input_glyph_col)->setValue(glyph.empty() ? LLSD() : LLSD(glyph));
}
}
// Fills the axis-channel Output cells (text description + SecondLife-game-control
// glyph) from the current device's axis map.
void LLPanelPreferenceGameControl::populateAxisChannelsCells()
{
std::vector<LLScrollListItem*> rows = mAxisChannels->getAllData();
const LLGameControl::Options& options = getSelectedDeviceOptions();
const auto& axis_map = options.getAxisMap();
S32 output_desc_col = mAxisChannels->getColumn("output_description")->mIndex;
S32 output_glyph_col = mAxisChannels->getColumn("output")->mIndex;
for (size_t i = 0; i < rows.size() && i < axis_map.size(); ++i)
{
std::string output_name = LLGameControl::axisOutputName(axis_map[i]);
std::string glyph = promptFontGlyph(output_name);
rows[i]->getColumn(output_desc_col)->setValue(inputLabel(mAxisOutputSelector, output_name));
rows[i]->getColumn(output_glyph_col)->setValue(glyph.empty() ? LLSD() : LLSD(glyph));
}
}
// Creates one row per physical button in the button-channels table. Columns:
// input(glyph) | input_description | output_description | output(glyph). The two
// Input columns are the fixed physical button (icon + text); the two Output columns
// are the canonical button it maps to (icon + text), edited via popup selectors.
void LLPanelPreferenceGameControl::populateButtonChannelsRows()
{
mButtonChannels->clearRows();
std::vector<LLScrollListItem*> items = mButtonInputSelector->getAllData();
S32 input_glyph_col = mButtonChannels->getColumn("input")->mIndex;
S32 input_desc_col = mButtonChannels->getColumn("input_description")->mIndex;
LLScrollListItem::Params row_params;
LLScrollListCell::Params cell_params;
for (S32 i = 0; i < (S32)(mButtonChannels->getNumColumns()); ++i)
{
const std::string& name = mButtonChannels->getColumn(i)->mName;
// The icon columns ("input"/"output") render the controller glyph in
// SecondLife-game-control; the description columns match the
// "Controls" panel font.
cell_params.font = (name == "input" || name == "output")
? promptFontForCell()
: LLFontGL::getFontSansSerif();
cell_params.column = name;
row_params.columns.add(cell_params);
}
for (size_t i = 0; i < LLGameControl::NUM_BUTTONS; ++i)
{
LLScrollListItem* row = mButtonChannels->addRow(row_params);
// The physical button is fixed: fill both its text label and its glyph
// (buttons without a glyph, e.g. spares, keep an empty icon cell).
std::string glyph = promptFontGlyph(items[i]->getValue().asString());
row->getColumn(input_desc_col)->setValue(items[i]->getColumn(0)->getValue());
row->getColumn(input_glyph_col)->setValue(glyph.empty() ? LLSD() : LLSD(glyph));
}
}
// Fills the button-channel Output cells (text description +
// SecondLife-game-control glyph) from the current device's button map.
void LLPanelPreferenceGameControl::populateButtonChannelsCells()
{
std::vector<LLScrollListItem*> rows = mButtonChannels->getAllData();
const auto& button_map = getSelectedDeviceOptions().getButtonMap();
std::vector<LLScrollListItem*> items = mButtonInputSelector->getAllData();
llassert(rows.size() == button_map.size());
S32 output_desc_col = mButtonChannels->getColumn("output_description")->mIndex;
S32 output_glyph_col = mButtonChannels->getColumn("output")->mIndex;
for (size_t i = 0; i < rows.size(); ++i)
{
U8 output = button_map[i];
rows[i]->getColumn(output_desc_col)->setValue(selectorLabelAt(mButtonInputSelector, output));
std::string value = (output < items.size()) ? items[output]->getValue().asString() : LLStringUtil::null;
std::string glyph = promptFontGlyph(value);
rows[i]->getColumn(output_glyph_col)->setValue(glyph.empty() ? LLSD() : LLSD(glyph));
}
}
// Rebuilds the Device State selector from the currently connected devices.
// Only connected devices are listed here, since the tab shows live input and a
// disconnected device has no live state. Shows the "No device" message and hides
// the tables when nothing is connected.
void LLPanelPreferenceGameControl::updateDeviceStateList()
{
mStateDeviceList->clear();
mStateDeviceList->clearRows();
const std::list<LLGameControl::Device>& devices = LLGameControl::getDevices();
bool hasDevice = !devices.empty();
mStateNoDeviceMessage->setVisible(!hasDevice);
mStateDevicePrompt->setVisible(hasDevice);
mStateRemapNote->setVisible(hasDevice);
mStateDeviceList->setVisible(hasDevice);
mRestoreDeviceOptionsDefaults->setVisible(hasDevice);
mPanelDeviceState->setVisible(hasDevice);
if (!hasDevice)
{
mStateSelectedDeviceGUID.clear();
return;
}
for (const LLGameControl::Device& device : devices)
{
std::string title = device.getGUID() + ", " + device.getName();
mStateDeviceList->addElement(LLSD().with("value", device.getGUID())
.with("columns", LLSD().with("label", title)));
}
// Keep the current selection if that device is still connected; otherwise
// fall back to the first device.
if (mStateSelectedDeviceGUID.empty() || !mStateDeviceList->setSelectedByValue(mStateSelectedDeviceGUID, true))
{
mStateDeviceList->selectNthItem(0);
}
mStateSelectedDeviceGUID = mStateDeviceList->getValue().asString();
// Refresh the editable axis option cells for the (re)selected state device.
populateAxisStateOptionCells();
}
// Creates one row per physical axis in the axis-state table. Columns:
// Axis-state table columns (input, raw_value, invert, offset, dead_zone,
// fixed_value); their display order is defined entirely by the XML. Raw/Adjusted
// are filled live in draw(); Invert/Offset/Dead Zone are editable axis options.
// All cells are addressed by column name (see axisStateColumn), so reordering the
// columns in the XML requires no change here.
void LLPanelPreferenceGameControl::populateAxisStateRows()
{
mAxisState->clearRows();
// One row per physical axis, labelled from the output selector's canonical-axis items.
std::vector<LLScrollListItem*> items = mAxisOutputSelector->getAllData();
LLScrollListItem::Params row_params;
LLScrollListCell::Params cell_params;
for (S32 i = 0; i < (S32)(mAxisState->getNumColumns()); ++i)
{
const std::string& name = mAxisState->getColumn(i)->mName;
// The icon column ("input_glyph") renders the controller glyph in
// SecondLife-game-control font; every other column uses the default text font.
cell_params.font = (name == "input_glyph")
? promptFontForCell()
: LLFontGL::getFontSansSerif();
cell_params.column = name;
row_params.columns.add(cell_params);
}
// Configure each cell by column name so the XML column order stays the single
// source of truth (reordering columns in the XML needs no change here).
row_params.columns(axisStateColumn("raw_value")).font_halign = "right";
row_params.columns(axisStateColumn("invert")).type = "checkbox";
row_params.columns(axisStateColumn("invert")).font_halign = "right"; // right-align the checkbox within the cell
row_params.columns(axisStateColumn("offset")).font_halign = "right";
row_params.columns(axisStateColumn("dead_zone")).font_halign = "right";
row_params.columns(axisStateColumn("fixed_value")).font_halign = "right";
S32 input_glyph_col = axisStateColumn("input_glyph");
S32 input_col = axisStateColumn("input");
for (size_t i = 0; i < LLGameControl::NUM_AXES; ++i)
{
LLScrollListItem* row = mAxisState->addRow(row_params);
// The physical axis is fixed: fill both its SecondLife-game-control glyph
// and its text label.
std::string glyph = promptFontGlyph(items[i]->getValue().asString());
row->getColumn(input_glyph_col)->setValue(glyph.empty() ? LLSD() : LLSD(glyph));
row->getColumn(input_col)->setValue(items[i]->getColumn(0)->getValue()); // physical axis label
}
// Fill the editable option columns for the currently selected state device.
populateAxisStateOptionCells();
}
// Fills the Device State axis table's editable option cells (Invert / Offset /
// Dead Zone) from the state tab's selected device. Called when rows are built and
// whenever the selected state device changes; the per-frame draw() only refreshes
// the Raw/Adjusted columns, so it never clobbers these while the user edits.
void LLPanelPreferenceGameControl::populateAxisStateOptionCells()
{
auto options_it = mDeviceOptions.find(mStateSelectedDeviceGUID);
if (options_it == mDeviceOptions.end())
{
return;
}
const auto& all_axis_options = options_it->second.options.getAxisOptions();
std::vector<LLScrollListItem*> rows = mAxisState->getAllData();
S32 invert_col = axisStateColumn("invert");
S32 offset_col = axisStateColumn("offset");
S32 dead_zone_col = axisStateColumn("dead_zone");
for (size_t i = 0; i < rows.size() && i < all_axis_options.size(); ++i)
{
const LLGameControl::Options::AxisOptions& axis_options = all_axis_options[i];
rows[i]->getColumn(invert_col)->setValue(axis_options.mMultiplier == -1); // Invert checkbox
setNumericLabel(rows[i]->getColumn(offset_col), axis_options.mOffset); // Offset
setNumericLabel(rows[i]->getColumn(dead_zone_col), axis_options.mDeadZone); // Dead Zone
}
}
// Creates one row per physical button in the button-state table.
// Columns: Button (physical button label) | Value (filled live in draw()).
void LLPanelPreferenceGameControl::populateButtonStateRows()
{
mButtonState->clearRows();
std::vector<LLScrollListItem*> items = mButtonInputSelector->getAllData();
S32 input_glyph_col = mButtonState->getColumn("input_glyph")->mIndex;
S32 input_col = mButtonState->getColumn("input")->mIndex;
LLScrollListItem::Params row_params;
LLScrollListCell::Params cell_params;
for (S32 i = 0; i < (S32)(mButtonState->getNumColumns()); ++i)
{
const std::string& name = mButtonState->getColumn(i)->mName;
// The icon column ("input_glyph") renders the controller glyph in
// SecondLife-game-control; every other column uses the default text font.
cell_params.font = (name == "input_glyph")
? promptFontForCell()
: LLFontGL::getFontSansSerif();
cell_params.column = name;
row_params.columns.add(cell_params);
}
row_params.columns(mButtonState->getColumn("value")->mIndex).font_halign = "right"; // Value
for (size_t i = 0; i < LLGameControl::NUM_BUTTONS; ++i)
{
LLScrollListItem* row = mButtonState->addRow(row_params);
// The physical button is fixed: fill both its SecondLife-game-control
// glyph and its text label.
std::string glyph = promptFontGlyph(items[i]->getValue().asString());
row->getColumn(input_glyph_col)->setValue(glyph.empty() ? LLSD() : LLSD(glyph));
row->getColumn(input_col)->setValue(items[i]->getColumn(0)->getValue()); // physical button label
}
}
// Fills the Value columns of both state tables from the selected device's live
// input state. Axis values are the raw [0, 32767] readings; button values are
// 1 (pressed) or 0 (released). Called each frame while the tab is visible.
void LLPanelPreferenceGameControl::populateDeviceStateValues()
{
if (mStateSelectedDeviceGUID.empty())
{
return;
}
// Locate the live state for the selected device by GUID.
const LLGameControl::State* state = nullptr;
for (const LLGameControl::Device& device : LLGameControl::getDevices())
{
if (device.getGUID() == mStateSelectedDeviceGUID)
{
state = &device.getState();
break;
}
}
if (!state)
{
return;
}
// Each row is a physical axis, and its Invert/Offset/Dead-Zone options are keyed by
// physical axis, so Raw/Adjusted must be too. State::mAxes/mRawAxes are keyed by
// canonical output axis (post-mapping), which would land on the wrong row; instead
// read mPhysicalRawAxes (pre-fix) / mPhysicalFixedAxes (post-fix), the pre-map values
// onAxis() preserves by physical index specifically for this table.
std::vector<LLScrollListItem*> axisRows = mAxisState->getAllData();
S32 raw_col = axisStateColumn("raw_value");
S32 fixed_col = axisStateColumn("fixed_value");
for (size_t i = 0; i < axisRows.size(); ++i)
{
if (i >= state->mPhysicalRawAxes.size())
{
break;
}
S32 raw = (S32)state->mPhysicalRawAxes[i]; // Raw Value (pre-fix)
S32 fixed = (S32)state->mPhysicalFixedAxes[i]; // Adjusted Value (post-fix)
axisRows[i]->getColumn(raw_col)->setValue(llformat("%d ", raw));
axisRows[i]->getColumn(fixed_col)->setValue(llformat("%d ", fixed));
}
// Each row is a physical button, so read the pre-map mPhysicalButtons; mButtons is
// keyed by canonical button (post-mapping) and would show state on the wrong row.
std::vector<LLScrollListItem*> buttonRows = mButtonState->getAllData();
S32 button_value_col = mButtonState->getColumn("value")->mIndex;
for (size_t i = 0; i < buttonRows.size(); ++i)
{
S32 value = (state->mPhysicalButtons >> i) & 0x1;
buttonRows[i]->getColumn(button_value_col)->setValue(llformat("%d ", value));
}
}
// Static hook, called from the send path each time a fresh GameControlInput
// message goes out. Refreshes the Data Output tab's Value column from the
// values just packed into that message (the current server state).
// sGameControlPanel exists for the lifetime of the panel instance, not just while
// it's on-screen (e.g. Preferences closed, or another tab selected), and this is
// called unconditionally on every outgoing message -- so skip the refresh (and the
// mode-change/row-rebuild check inside populateDataOutputValues()) unless the tab is
// actually visible; it will simply pick up the current values on the next send after
// the user switches back to it.
void LLPanelPreferenceGameControl::updateDataOutput()
{
if (sGameControlPanel && sGameControlPanel->mTabDataOutput->isInVisibleChain())
{
sGameControlPanel->populateDataOutputValues();
}
}
// Creates the Data Output rows: one per canonical axis (AXIS_0..AXIS_5), a single
// empty separator row, then one per canonical button (BUTTON_0..BUTTON_31), then
// the semantic (mode-dependent) block built by populateDataOutputSemanticRows().
// The Value column is filled later in populateDataOutputValues().
void LLPanelPreferenceGameControl::populateDataOutputRows()
{
mDataOutput->clearRows();
mDataOutputSemanticMode = -1; // force the semantic block below to build fresh
LLScrollListItem::Params row_params;
LLScrollListCell::Params cell_params;
cell_params.font = LLFontGL::getFontSansSerif();
for (S32 i = 0; i < (S32)(mDataOutput->getNumColumns()); ++i)
{
cell_params.column = mDataOutput->getColumn(i)->mName;
row_params.columns.add(cell_params);
}
row_params.columns(2).font_halign = "right"; // Value
// Column 0 is a per-block 0-based index; it restarts at 0 for the button
// block below. Column 1 is the canonical output name, column 2 the Value.
for (size_t i = 0; i < LLGameControl::NUM_AXES; ++i)
{
LLScrollListItem* row = mDataOutput->addRow(row_params);
row->getColumn(0)->setValue(llformat("%d", (S32)i));
row->getColumn(1)->setValue(LLGameControl::axisOutputName((U8)i));
}
// One empty row separates the axis block from the button block.
mDataOutput->addRow(row_params);
for (size_t i = 0; i < LLGameControl::NUM_BUTTONS; ++i)
{
LLScrollListItem* row = mDataOutput->addRow(row_params);
row->getColumn(0)->setValue(llformat("%d", (S32)i));
// getRemoteName() gives the Button enum's symbolic name for the named
// buttons (0..BUTTON_TOUCHPAD) but blanks the unnamed tail; fall back to
// the indexed BUTTON_N form there, which is exactly the enum name for those.
std::string name = LLGameControl::InputChannel(LLGameControl::InputChannel::TYPE_BUTTON, (U8)i).getRemoteName();
LLStringUtil::trim(name);
if (name.empty())
{
name = llformat("BUTTON_%d", (S32)i);
}
row->getColumn(1)->setValue(name);
}
populateDataOutputSemanticRows((LLGameControl::AgentControlMode)LLGameControl::getServerState().mActionMode);
}
// Rebuilds the semantic (mode-dependent) block of Data Output rows -- a blank
// separator, one row per semantic axis slot for 'mode' (LLGameControl::
// numSemanticAxesForMode()/semanticAxisName()), another blank separator, then one
// row per semantic button slot (numSemanticButtonsForMode()/semanticButtonName()).
// Called whenever populateDataOutputValues() finds the live ServerState::mActionMode
// no longer matches mDataOutputSemanticMode: unlike the canonical block above, both
// the semantic block's row count and its labels depend on the mode, so simply
// truncating/re-adding rows here (rather than reusing stale ones) is what keeps a
// mode switch from leaving behind a stale label or a Value from a slot that no
// longer exists for the new mode.
void LLPanelPreferenceGameControl::populateDataOutputSemanticRows(LLGameControl::AgentControlMode mode)
{
static const S32 CANONICAL_ROW_COUNT = (S32)LLGameControl::NUM_AXES + 1 + (S32)LLGameControl::NUM_BUTTONS;
while (mDataOutput->getItemCount() > CANONICAL_ROW_COUNT)
{
mDataOutput->deleteSingleItem(mDataOutput->getItemCount() - 1);
}
LLScrollListItem::Params row_params;
LLScrollListCell::Params cell_params;
cell_params.font = LLFontGL::getFontSansSerif();
for (S32 i = 0; i < (S32)(mDataOutput->getNumColumns()); ++i)
{
cell_params.column = mDataOutput->getColumn(i)->mName;
row_params.columns.add(cell_params);
}
row_params.columns(2).font_halign = "right"; // Value
// One empty row separates the canonical button block from the semantic axes.
mDataOutput->addRow(row_params);
U8 num_semantic_axes = LLGameControl::numSemanticAxesForMode(mode);
for (U8 i = 0; i < num_semantic_axes; ++i)
{
LLScrollListItem* row = mDataOutput->addRow(row_params);
row->getColumn(0)->setValue(llformat("%d", (S32)i));
row->getColumn(1)->setValue(LLGameControl::semanticAxisName(mode, i));
}
// One empty row separates the semantic axes from the semantic buttons.
mDataOutput->addRow(row_params);
U8 num_semantic_buttons = LLGameControl::numSemanticButtonsForMode(mode);
for (U8 i = 0; i < num_semantic_buttons; ++i)
{
LLScrollListItem* row = mDataOutput->addRow(row_params);
row->getColumn(0)->setValue(llformat("%d", (S32)i));
row->getColumn(1)->setValue(LLGameControl::semanticButtonName(mode, i));
}
mDataOutputSemanticMode = (S32)mode;
}
// Fills the Data Output Value column from the last outgoing server state: each
// axis shows its signed [-32768, 32767] value; each button shows 1 (pressed) or
// 0 (released). The row layout matches populateDataOutputRows()/
// populateDataOutputSemanticRows(): canonical axes, a blank separator, canonical
// buttons, a blank separator, semantic axes, a blank separator, semantic buttons.
void LLPanelPreferenceGameControl::populateDataOutputValues()
{
const LLGameControl::ServerState& state = LLGameControl::getServerState();
LLGameControl::AgentControlMode mode = (LLGameControl::AgentControlMode)state.mActionMode;
if ((S32)mode != mDataOutputSemanticMode)
{
populateDataOutputSemanticRows(mode);
}
std::vector<LLScrollListItem*> rows = mDataOutput->getAllData();
size_t row = 0;
for (size_t i = 0; i < LLGameControl::NUM_AXES && i < state.mAxes.size() && row < rows.size(); ++i, ++row)
{
rows[row]->getColumn(2)->setValue(llformat("%d ", (S32)state.mAxes[i]));
}
++row; // skip the empty separator row
for (size_t i = 0; i < LLGameControl::NUM_BUTTONS && row < rows.size(); ++i, ++row)
{
S32 value = (state.mButtons >> i) & 0x1;
rows[row]->getColumn(2)->setValue(llformat("%d ", value));
}
++row; // skip the empty separator row between canonical buttons and semantic axes
U8 num_semantic_axes = LLGameControl::numSemanticAxesForMode(mode);
for (size_t i = 0; i < num_semantic_axes && i < state.mSemanticAxes.size() && row < rows.size(); ++i, ++row)
{
rows[row]->getColumn(2)->setValue(llformat("%d ", (S32)state.mSemanticAxes[i]));
}
++row; // skip the empty separator row between semantic axes and semantic buttons
U8 num_semantic_buttons = LLGameControl::numSemanticButtonsForMode(mode);
for (size_t i = 0; i < num_semantic_buttons && row < rows.size(); ++i, ++row)
{
S32 value = (state.mSemanticButtons >> i) & 0x1;
rows[row]->getColumn(2)->setValue(llformat("%d ", value));
}
}
// Returns the display label of the input selector item with the given value,
// or empty if there is no such item.
std::string LLPanelPreferenceGameControl::inputLabel(const LLComboBox* input_selector, const std::string& input_value)
{
for (const LLScrollListItem* item : input_selector->getAllData())
{
if (item->getValue().asString() == input_value)
{
return item->getColumn(0)->getValue().asString();
}
}
return LLStringUtil::null;
}
// Returns the display label of the selector item at the given index (the item
// position matches the canonical axis/button index), or empty if out of range.
std::string LLPanelPreferenceGameControl::selectorLabelAt(const LLComboBox* selector, S32 index)
{
std::vector<LLScrollListItem*> items = selector->getAllData();
if (index < 0 || index >= (S32)items.size())
{
return LLStringUtil::null;
}
return items[index]->getColumn(0)->getValue().asString();
}
// Populates glyph_selector with one item per value in text_selector. Each item
// keeps the canonical value but is displayed as its SecondLife-game-control
// glyph, so the dropdown shown over the icon column matches the icons in the
// table. Inputs with no glyph (e.g. "None", spare buttons) keep their text
// label in the normal font so they remain readable and selectable.
void LLPanelPreferenceGameControl::buildInputGlyphSelector(const LLComboBox* text_selector, LLComboBox* glyph_selector)
{
glyph_selector->clearRows();
for (const LLScrollListItem* item : text_selector->getAllData())
{
std::string value = item->getValue().asString();
std::string glyph = promptFontGlyph(value);
LLSD element;
element["value"] = value;
LLSD& column = element["columns"][0];
column["column"] = "input";
if (glyph.empty())
{
column["value"] = item->getColumn(0)->getValue().asString(); // text label
column["font"]["name"] = "SansSerif";
column["font"]["size"] = "Small";
}
else
{
column["value"] = glyph;
column["font"]["name"] = "SecondLife-game-control";
column["font"]["size"] = "Huge";
}
glyph_selector->addElement(element);
}
}
// Returns the options struct for the currently selected device.
LLGameControl::Options& LLPanelPreferenceGameControl::getSelectedDeviceOptions()
{
auto options_it = mDeviceOptions.find(mSelectedDeviceGUID);
llassert_always(options_it != mDeviceOptions.end());
return options_it->second.options;
}
// Formats a numeric value for display in a table cell.
// Always shows the value, including the zero (default) case.
S32 LLPanelPreferenceGameControl::axisStateColumn(const std::string& name) const
{
LLScrollListColumn* column = mAxisState->getColumn(name);
return column ? column->mIndex : -1;
}
void LLPanelPreferenceGameControl::setNumericLabel(LLScrollListCell* cell, S32 value)
{
cell->setValue(llformat("%d ", value));
}
// Positions a UI control (combobox or spin control) to overlay a specific table cell.
// Used to show editors inline within the scroll list tables.
void LLPanelPreferenceGameControl::fitInRect(LLUICtrl* ctrl, LLScrollListCtrl* grid, S32 row_index, S32 col_index)
{
LLRect rect(grid->getCellRect(row_index, col_index));
LLView* parent = grid->getParent();
while (parent && parent != ctrl->getParent())
{
rect.translate(parent->getRect().mLeft, parent->getRect().mBottom);
parent = parent->getParent();
}
ctrl->setRect(rect);
rect.translate(-rect.mLeft, -rect.mBottom);
for (LLView* child : *ctrl->getChildList())
{
LLRect childRect(child->getRect());
childRect.intersectWith(rect);
if (childRect.mRight < rect.mRight &&
childRect.mRight > (rect.mLeft + rect.mRight) / 2)
{
childRect.mRight = rect.mRight;
}
child->setRect(childRect);
}
}
// Clears the current cell selection state and hides all popup editors.
void LLPanelPreferenceGameControl::clearSelectionState()
{
// Restore the text we blanked when the selector opened (initCombobox).
// On commit this is the freshly committed value; otherwise it's the
// original, so a dismissed selector leaves the cell unchanged.
if (sSelectedCell)
{
sSelectedCell->setValue(sSelectedCellValue);
}
sSelectedCellValue = LLSD();
if (sSelectedGrid)
{
sSelectedGrid->deselectAllItems(true);
}
sSelectedGrid = nullptr;
sSelectedItem = nullptr;
sSelectedCell = nullptr;
sSelectedCombobox = nullptr;
sSelectedRow = -1;
sSelectedCol = -1;
mNumericValueEditor->setVisible(false);
mAxisInputSelector->setVisible(false);
mAxisOutputSelector->setVisible(false);
mButtonInputSelector->setVisible(false);
mAxisInputGlyphSelector->setVisible(false);
mButtonInputGlyphSelector->setVisible(false);
mAxisOutputGlyphSelector->setVisible(false);
}
// Resets the current mode's axis + button action mappings to the built-in defaults.
void LLPanelPreferenceGameControl::onResetActionsToDefaults()
{
clearSelectionState();
std::string mode = currentEditMode();
LLSD defaults = LLGameControl::getDefaultModeMappings();
LLGameControl::setModeMapping(mode, MODE_INPUT_TYPE_AXES, defaults[mode][MODE_INPUT_TYPE_AXES]);
LLGameControl::setModeMapping(mode, MODE_INPUT_TYPE_BUTTONS, defaults[mode][MODE_INPUT_TYPE_BUTTONS]);
LLGameControl::setModeMapping(mode, MODE_INPUT_TYPE_AXES_INVERT, defaults[mode][MODE_INPUT_TYPE_AXES_INVERT]);
// AllowRoll, SpeedFactor, ShowCrosshair, and HideCursor live outside the
// Axes/Buttons/AxesInvert mapping this function otherwise restores, so
// they need to be reset explicitly.
if (mode == "FlyCam")
{
LLGameControl::setFlycamRollAllowed(false);
LLGameControl::setFlycamCrosshairEnabled(false);
LLGameControl::setFlycamHideCursorEnabled(true);
}
if (mode == "FlyCam" || mode == "Mouselook" || mode == "Avatar")
{
LLGameControl::setSpeedFactor(mode, 1.0f);
}
populateActionMappings();
updateActionModeEnabledUI(); // syncs mCheckFlycamAllowRoll with the reset value above
// Push the pending UI state into LLGameControl's runtime so the effect is
// immediate. gSavedSettings is updated later via saveSettings() on OK.
LLGameControl::applySettingsFromLLSD(getSettingsAsLLSD());
}
// Resets the selected device's options (axis/button remap, invert, deadzone, offset)
// to defaults.
void LLPanelPreferenceGameControl::onResetDeviceToDefaults()
{
clearSelectionState();
if (mSelectedDeviceGUID.empty())
{
return;
}
LLGameControl::Options& options = getSelectedDeviceOptions();
options.resetToDefaults();
LLGameControl::setDeviceOptions(mSelectedDeviceGUID, options);
populateAxisChannelsCells();
populateButtonChannelsCells();
// Invert/offset/dead-zone now live on the Device State tab; refresh them too
// (a no-op unless the reset device is the one shown there).
populateAxisStateOptionCells();
LLGameControl::applySettingsFromLLSD(getSettingsAsLLSD());
}
// Resets the state-tab device's per-axis tuning (Invert / Offset / Dead Zone) to
// defaults for every axis, leaving the axis/button remaps untouched. This backs
// the Device Options tab's "Restore Defaults" button.
void LLPanelPreferenceGameControl::onResetDeviceOptionsToDefaults()
{
clearSelectionState();
auto options_it = mDeviceOptions.find(mStateSelectedDeviceGUID);
if (options_it == mDeviceOptions.end())
{
return;
}
LLGameControl::Options& options = options_it->second.options;
for (LLGameControl::Options::AxisOptions& axis_options : options.getAxisOptions())
{
axis_options.resetToDefaults();
}
LLGameControl::setDeviceOptions(mStateSelectedDeviceGUID, options);
populateAxisStateOptionCells();
LLGameControl::applySettingsFromLLSD(getSettingsAsLLSD());
}
// Starts a new auto-calibration pass for the state tab's selected device: clears any
// previously-collected samples and lets updateAutoCalibration() (called from draw())
// gather CALIBRATION_SAMPLE_COUNT raw-axis readings, one per frame while the Device
// Options tab is visible.
void LLPanelPreferenceGameControl::onAutoCalibrate()
{
clearSelectionState();
if (mStateSelectedDeviceGUID.empty())
{
return;
}
mCalibrationSamples.assign(LLGameControl::NUM_AXES, {});
mCalibrationSamplesCollected = 0;
mCalibrating = true;
}
// Advances an in-progress auto-calibration pass by one frame: records the state
// device's current raw axis readings, and once CALIBRATION_SAMPLE_COUNT frames have
// been sampled, derives Offset/Dead Zone for every axis whose readings never changed
// across all samples (i.e. the axis wasn't being manipulated, so its readings are its
// natural "zero input" state). Axes that changed are left untouched.
void LLPanelPreferenceGameControl::updateAutoCalibration()
{
// Locate the live state for the selected device by GUID (same lookup as
// populateDeviceStateValues()).
const LLGameControl::State* state = nullptr;
for (const LLGameControl::Device& device : LLGameControl::getDevices())
{
if (device.getGUID() == mStateSelectedDeviceGUID)
{
state = &device.getState();
break;
}
}
if (!state)
{
mCalibrating = false;
return;
}
for (size_t i = 0; i < mCalibrationSamples.size() && i < state->mPhysicalRawAxes.size(); ++i)
{
mCalibrationSamples[i].push_back(state->mPhysicalRawAxes[i]);
}
++mCalibrationSamplesCollected;
if (mCalibrationSamplesCollected < CALIBRATION_SAMPLE_COUNT)
{
return;
}
mCalibrating = false;
auto options_it = mDeviceOptions.find(mStateSelectedDeviceGUID);
if (options_it != mDeviceOptions.end())
{
std::vector<LLGameControl::Options::AxisOptions>& all_axis_options = options_it->second.options.getAxisOptions();
for (size_t i = 0; i < mCalibrationSamples.size() && i < all_axis_options.size(); ++i)
{
const std::vector<S16>& samples = mCalibrationSamples[i];
bool steady = !samples.empty();
for (size_t s = 1; s < samples.size() && steady; ++s)
{
steady = (samples[s] == samples[0]);
}
if (!steady)
{
// Axis moved during the measurement window: the user is manipulating
// it, so leave its Offset/Dead Zone untouched.
continue;
}
S32 offset = std::clamp<S32>(-(S32)samples[0], -(S32)(LLGameControl::MAX_AXIS_OFFSET), (S32)(LLGameControl::MAX_AXIS_OFFSET));
S32 dead_zone = std::clamp(2 * std::abs(offset), (S32)(LLGameControl::DEFAULT_AXIS_DEAD_ZONE), (S32)(LLGameControl::MAX_AXIS_DEAD_ZONE));
LLGameControl::Options::AxisOptions& axis_options = all_axis_options[i];
axis_options.mOffset = (S16)offset;
axis_options.mDeadZone = (U16)dead_zone;
}
LLGameControl::setDeviceOptions(mStateSelectedDeviceGUID, options_it->second.options);
populateAxisStateOptionCells();
LLGameControl::applySettingsFromLLSD(getSettingsAsLLSD());
}
mCalibrationSamples.clear();
mCalibrationSamplesCollected = 0;
}
// Captures current settings values into mOrigSettings for later restoration upon cancel().
void LLPanelPreferenceGameControl::rememberOriginalSettings()
{
mOrigSettings = LLGameControl::getSettingsAsLLSD();
}
|