A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR
2using System.Linq;
3using UnityEditor;
4using UnityEngine.UIElements;
5using UnityEngine.InputSystem.Layouts;
6using UnityEngine.InputSystem.Utilities;
7using System.Collections.Generic;
8
9namespace UnityEngine.InputSystem.Editor
10{
11 internal class MatchingControlPath
12 {
13 public string deviceName
14 {
15 get;
16 }
17 public string controlName
18 {
19 get;
20 }
21
22 public bool isRoot
23 {
24 get;
25 }
26 public List<MatchingControlPath> children
27 {
28 get;
29 }
30
31
32 public MatchingControlPath(string deviceName, string controlName, bool isRoot)
33 {
34 this.deviceName = deviceName;
35 this.controlName = controlName;
36 this.isRoot = isRoot;
37 this.children = new List<MatchingControlPath>();
38 }
39
40#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
41 public static List<TreeViewItemData<MatchingControlPath>> BuildMatchingControlPathsTreeData(List<MatchingControlPath> matchingControlPaths)
42 {
43 int id = 0;
44 return BuildMatchingControlPathsTreeDataRecursive(ref id, matchingControlPaths);
45 }
46
47 private static List<TreeViewItemData<MatchingControlPath>> BuildMatchingControlPathsTreeDataRecursive(ref int id, List<MatchingControlPath> matchingControlPaths)
48 {
49 var treeViewList = new List<TreeViewItemData<MatchingControlPath>>(matchingControlPaths.Count);
50 foreach (var matchingControlPath in matchingControlPaths)
51 {
52 var childTreeViewList = BuildMatchingControlPathsTreeDataRecursive(ref id, matchingControlPath.children);
53
54 var treeViewItem = new TreeViewItemData<MatchingControlPath>(id++, matchingControlPath, childTreeViewList);
55 treeViewList.Add(treeViewItem);
56 }
57
58 return treeViewList;
59 }
60
61#endif
62
63 public static List<MatchingControlPath> CollectMatchingControlPaths(string path, bool showPaths, ref bool controlPathUsagePresent)
64 {
65 var matchingControlPaths = new List<MatchingControlPath>();
66
67 if (path == string.Empty)
68 return matchingControlPaths;
69
70 var deviceLayoutPath = InputControlPath.TryGetDeviceLayout(path);
71 var parsedPath = InputControlPath.Parse(path).ToArray();
72
73 // If the provided path is parseable into device and control components, draw UI which shows control layouts that match the path.
74 if (parsedPath.Length >= 2 && !string.IsNullOrEmpty(deviceLayoutPath))
75 {
76 bool matchExists = false;
77
78 var rootDeviceLayout = EditorInputControlLayoutCache.TryGetLayout(deviceLayoutPath);
79 bool isValidDeviceLayout = deviceLayoutPath == InputControlPath.Wildcard || (rootDeviceLayout != null && !rootDeviceLayout.isOverride && !rootDeviceLayout.hideInUI);
80 // Exit early if a malformed device layout was provided,
81 if (!isValidDeviceLayout)
82 return matchingControlPaths;
83
84 controlPathUsagePresent = parsedPath[1].usages.Count() > 0;
85 bool hasChildDeviceLayouts = deviceLayoutPath == InputControlPath.Wildcard || EditorInputControlLayoutCache.HasChildLayouts(rootDeviceLayout.name);
86
87 // If the path provided matches exactly one control path (i.e. has no ui-facing child device layouts or uses control usages), then exit early
88 if (!controlPathUsagePresent && !hasChildDeviceLayouts)
89 return matchingControlPaths;
90
91 // Otherwise, we will show either all controls that match the current binding (if control usages are used)
92 // or all controls in derived device layouts (if a no control usages are used).
93
94 // If our control path contains a usage, make sure we render the binding that belongs to the root device layout first
95 if (deviceLayoutPath != InputControlPath.Wildcard && controlPathUsagePresent)
96 {
97 matchExists |= CollectMatchingControlPathsForLayout(rootDeviceLayout, in parsedPath, true, matchingControlPaths);
98 }
99 // Otherwise, just render the bindings that belong to child device layouts. The binding that matches the root layout is
100 // already represented by the user generated control path itself.
101 else
102 {
103 IEnumerable<InputControlLayout> matchedChildLayouts = Enumerable.Empty<InputControlLayout>();
104 if (deviceLayoutPath == InputControlPath.Wildcard)
105 {
106 matchedChildLayouts = EditorInputControlLayoutCache.allLayouts
107 .Where(x => x.isDeviceLayout && !x.hideInUI && !x.isOverride && x.isGenericTypeOfDevice && x.baseLayouts.Count() == 0).OrderBy(x => x.displayName);
108 }
109 else
110 {
111 matchedChildLayouts = EditorInputControlLayoutCache.TryGetChildLayouts(rootDeviceLayout.name);
112 }
113
114 foreach (var childLayout in matchedChildLayouts)
115 {
116 matchExists |= CollectMatchingControlPathsForLayout(childLayout, in parsedPath, false, matchingControlPaths);
117 }
118 }
119
120 // Otherwise, indicate that no layouts match the current path.
121 if (!matchExists)
122 {
123 return null;
124 }
125 }
126
127 return matchingControlPaths;
128 }
129
130 /// <summary>
131 /// Returns true if the deviceLayout or any of its children has controls which match the provided parsed path. exist matching registered control paths.
132 /// </summary>
133 /// <param name="deviceLayout">The device layout to draw control paths for</param>
134 /// <param name="parsedPath">The parsed path containing details of the Input Controls that can be matched</param>
135 private static bool CollectMatchingControlPathsForLayout(InputControlLayout deviceLayout, in InputControlPath.ParsedPathComponent[] parsedPath, bool isRoot, List<MatchingControlPath> matchingControlPaths)
136 {
137 string deviceName = deviceLayout.displayName;
138 string controlName = string.Empty;
139 bool matchExists = false;
140
141 for (int i = 0; i < deviceLayout.m_Controls.Length; i++)
142 {
143 ref InputControlLayout.ControlItem controlItem = ref deviceLayout.m_Controls[i];
144 if (InputControlPath.MatchControlComponent(ref parsedPath[1], ref controlItem, true))
145 {
146 // If we've already located a match, append a ", " to the control name
147 // This is to accomodate cases where multiple control items match the same path within a single device layout
148 // Note, some controlItems have names but invalid displayNames (i.e. the Dualsense HID > leftTriggerButton)
149 // There are instance where there are 2 control items with the same name inside a layout definition, however they are not
150 // labeled significantly differently.
151 // The notable example is that the Android Xbox and Android Dualshock layouts have 2 d-pad definitions, one is a "button"
152 // while the other is an axis.
153 controlName += matchExists ? $", {controlItem.name}" : controlItem.name;
154
155 // if the parsePath has a 3rd component, try to match it with items in the controlItem's layout definition.
156 if (parsedPath.Length == 3)
157 {
158 var controlLayout = EditorInputControlLayoutCache.TryGetLayout(controlItem.layout);
159 if (controlLayout.isControlLayout && !controlLayout.hideInUI)
160 {
161 for (int j = 0; j < controlLayout.m_Controls.Count(); j++)
162 {
163 ref InputControlLayout.ControlItem controlLayoutItem = ref controlLayout.m_Controls[j];
164 if (InputControlPath.MatchControlComponent(ref parsedPath[2], ref controlLayoutItem))
165 {
166 controlName += $"/{controlLayoutItem.name}";
167 matchExists = true;
168 }
169 }
170 }
171 }
172 else
173 {
174 matchExists = true;
175 }
176 }
177 }
178
179 IEnumerable<InputControlLayout> matchedChildLayouts = EditorInputControlLayoutCache.TryGetChildLayouts(deviceLayout.name);
180
181 // If this layout does not have a match, or is the top level root layout,
182 // skip over trying to draw any items for it, and immediately try processing the child layouts
183 if (!matchExists)
184 {
185 foreach (var childLayout in matchedChildLayouts)
186 {
187 matchExists |= CollectMatchingControlPathsForLayout(childLayout, in parsedPath, false, matchingControlPaths);
188 }
189 }
190 // Otherwise, draw the items for it, and then only process the child layouts if the foldout is expanded.
191 else
192 {
193 var newMatchingControlPath = new MatchingControlPath(deviceName, controlName, isRoot);
194 matchingControlPaths.Add(newMatchingControlPath);
195
196 foreach (var childLayout in matchedChildLayouts)
197 {
198 CollectMatchingControlPathsForLayout(childLayout, in parsedPath, false, newMatchingControlPath.children);
199 }
200 }
201
202 return matchExists;
203 }
204 }
205}
206#endif