Skip to content

Commit d46ba1a

Browse files
authored
NEW: Add UI Toolkit matching control bindings UI (#1835)
* Add 'matching controls' into tree view of new UI TK action asset UI * Tightened up layout of control scheme text and the matching paths tree view labels * Split out MatchingControlPaths to separate source file
1 parent 1e71287 commit d46ba1a

File tree

6 files changed

+331
-143
lines changed

6 files changed

+331
-143
lines changed

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ however, it has to be formatted properly to pass verification tests.
2121
- [`InputAction.WasCompletedThisFrame`](xref:UnityEngine.InputSystem.InputAction.WasCompletedThisFrame) returns `true` on the frame that the action stopped being in the performed phase. This allows for similar functionality to [`WasPressedThisFrame`](xref:UnityEngine.InputSystem.InputAction.WasPressedThisFrame)/[`WasReleasedThisFrame`](xref:UnityEngine.InputSystem.InputAction.WasReleasedThisFrame) when paired with [`WasPerformedThisFrame`](xref:UnityEngine.InputSystem.InputAction.WasPerformedThisFrame) except it is directly based on the interactions driving the action. For example, you can use it to distinguish between the button being released or whether it was released after being held for long enough to perform when using the Hold interaction.
2222
- Added Copy, Paste and Cut support for Action Maps, Actions and Bindings via context menu and key command shortcuts.
2323
- Added Dual Sense Edge controller to be mapped to the same layout as the Dual Sense controller
24+
- UI Toolkit input action editor now supports showing the derived bindings.
2425

2526
### Fixed
2627
- Fixed syntax of code examples in API documentation for [`AxisComposite`](xref:UnityEngine.InputSystem.Composites.AxisComposite).

Packages/com.unity.inputsystem/InputSystem/Editor/AssetEditor/InputBindingPropertiesView.cs

Lines changed: 32 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -117,171 +117,61 @@ protected override void DrawGeneralProperties()
117117
private static bool showMatchingLayouts = false;
118118
private static Dictionary<string, bool> showMatchingChildLayouts = new Dictionary<string, bool>();
119119

120-
/// <summary>
121-
/// Finds all registered control paths implemented by concrete classes which match the current binding path and renders it.
122-
/// </summary>
123-
private void DrawMatchingControlPaths()
120+
private static void DrawMatchingControlPaths(List<MatchingControlPath> matchingControlPaths)
124121
{
125-
var path = m_ControlPathEditor.pathProperty.stringValue;
126-
if (path == string.Empty)
127-
return;
128-
129-
var deviceLayoutPath = InputControlPath.TryGetDeviceLayout(path);
130-
var parsedPath = InputControlPath.Parse(path).ToArray();
131-
132-
// If the provided path is parseable into device and control components, draw UI which shows control layouts that match the path.
133-
if (parsedPath.Length >= 2 && !string.IsNullOrEmpty(deviceLayoutPath))
122+
foreach (var matchingControlPath in matchingControlPaths)
134123
{
135-
bool matchExists = false;
124+
bool showLayout = false;
125+
EditorGUI.indentLevel++;
136126

137-
var rootDeviceLayout = EditorInputControlLayoutCache.TryGetLayout(deviceLayoutPath);
138-
bool isValidDeviceLayout = deviceLayoutPath == InputControlPath.Wildcard || (rootDeviceLayout != null && !rootDeviceLayout.isOverride && !rootDeviceLayout.hideInUI);
139-
// Exit early if a malformed device layout was provided,
140-
if (!isValidDeviceLayout)
141-
return;
127+
var text = $"{matchingControlPath.deviceName} > {matchingControlPath.controlName}";
128+
if (matchingControlPath.children.Count() > 0 && !matchingControlPath.isRoot)
129+
{
130+
showMatchingChildLayouts.TryGetValue(matchingControlPath.deviceName, out showLayout);
131+
showMatchingChildLayouts[matchingControlPath.deviceName] = EditorGUILayout.Foldout(showLayout, text);
132+
}
133+
else
134+
{
135+
EditorGUILayout.LabelField(text);
136+
}
142137

143-
bool controlPathUsagePresent = parsedPath[1].usages.Count() > 0;
144-
bool hasChildDeviceLayouts = deviceLayoutPath == InputControlPath.Wildcard || EditorInputControlLayoutCache.HasChildLayouts(rootDeviceLayout.name);
138+
showLayout |= matchingControlPath.isRoot;
139+
if (showLayout)
140+
DrawMatchingControlPaths(matchingControlPath.children);
145141

146-
// 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
147-
if (!controlPathUsagePresent && !hasChildDeviceLayouts)
148-
return;
142+
EditorGUI.indentLevel--;
143+
}
144+
}
149145

150-
// Otherwise, we will show either all controls that match the current binding (if control usages are used)
151-
// or all controls in derived device layouts (if a no control usages are used).
146+
/// <summary>
147+
/// Finds all registered control paths implemented by concrete classes which match the current binding path and renders it.
148+
/// </summary>
149+
private void DrawMatchingControlPaths()
150+
{
151+
bool controlPathUsagePresent = false;
152+
List<MatchingControlPath> matchingControlPaths = MatchingControlPath.CollectMatchingControlPaths(m_ControlPathEditor.pathProperty.stringValue, showMatchingLayouts, ref controlPathUsagePresent);
153+
if (matchingControlPaths == null || matchingControlPaths.Count != 0)
154+
{
152155
EditorGUILayout.BeginVertical();
153156
showMatchingLayouts = EditorGUILayout.Foldout(showMatchingLayouts, "Derived Bindings");
154157

155158
if (showMatchingLayouts)
156159
{
157-
// If our control path contains a usage, make sure we render the binding that belongs to the root device layout first
158-
if (deviceLayoutPath != InputControlPath.Wildcard && controlPathUsagePresent)
159-
{
160-
matchExists |= DrawMatchingControlPathsForLayout(rootDeviceLayout, in parsedPath, true);
161-
}
162-
// Otherwise, just render the bindings that belong to child device layouts. The binding that matches the root layout is
163-
// already represented by the user generated control path itself.
164-
else
165-
{
166-
IEnumerable<InputControlLayout> matchedChildLayouts = Enumerable.Empty<InputControlLayout>();
167-
if (deviceLayoutPath == InputControlPath.Wildcard)
168-
{
169-
matchedChildLayouts = EditorInputControlLayoutCache.allLayouts
170-
.Where(x => x.isDeviceLayout && !x.hideInUI && !x.isOverride && x.isGenericTypeOfDevice && x.baseLayouts.Count() == 0).OrderBy(x => x.displayName);
171-
}
172-
else
173-
{
174-
matchedChildLayouts = EditorInputControlLayoutCache.TryGetChildLayouts(rootDeviceLayout.name);
175-
}
176-
177-
foreach (var childLayout in matchedChildLayouts)
178-
{
179-
matchExists |= DrawMatchingControlPathsForLayout(childLayout, in parsedPath);
180-
}
181-
}
182-
183-
// Otherwise, indicate that no layouts match the current path.
184-
if (!matchExists)
160+
if (matchingControlPaths == null)
185161
{
186162
if (controlPathUsagePresent)
187163
EditorGUILayout.HelpBox("No registered controls match this current binding. Some controls are only registered at runtime.", MessageType.Warning);
188164
else
189165
EditorGUILayout.HelpBox("No other registered controls match this current binding. Some controls are only registered at runtime.", MessageType.Warning);
190166
}
191-
}
192-
193-
EditorGUILayout.EndVertical();
194-
}
195-
}
196-
197-
/// <summary>
198-
/// Returns true if the deviceLayout or any of its children has controls which match the provided parsed path. exist matching registered control paths.
199-
/// </summary>
200-
/// <param name="deviceLayout">The device layout to draw control paths for</param>
201-
/// <param name="parsedPath">The parsed path containing details of the Input Controls that can be matched</param>
202-
private bool DrawMatchingControlPathsForLayout(InputControlLayout deviceLayout, in InputControlPath.ParsedPathComponent[] parsedPath, bool isRoot = false)
203-
{
204-
string deviceName = deviceLayout.displayName;
205-
string controlName = string.Empty;
206-
bool matchExists = false;
207-
208-
for (int i = 0; i < deviceLayout.m_Controls.Length; i++)
209-
{
210-
ref InputControlLayout.ControlItem controlItem = ref deviceLayout.m_Controls[i];
211-
if (InputControlPath.MatchControlComponent(ref parsedPath[1], ref controlItem, true))
212-
{
213-
// If we've already located a match, append a ", " to the control name
214-
// This is to accomodate cases where multiple control items match the same path within a single device layout
215-
// Note, some controlItems have names but invalid displayNames (i.e. the Dualsense HID > leftTriggerButton)
216-
// There are instance where there are 2 control items with the same name inside a layout definition, however they are not
217-
// labeled significantly differently.
218-
// The notable example is that the Android Xbox and Android Dualshock layouts have 2 d-pad definitions, one is a "button"
219-
// while the other is an axis.
220-
controlName += matchExists ? $", {controlItem.name}" : controlItem.name;
221-
222-
// if the parsePath has a 3rd component, try to match it with items in the controlItem's layout definition.
223-
if (parsedPath.Length == 3)
224-
{
225-
var controlLayout = EditorInputControlLayoutCache.TryGetLayout(controlItem.layout);
226-
if (controlLayout.isControlLayout && !controlLayout.hideInUI)
227-
{
228-
for (int j = 0; j < controlLayout.m_Controls.Count(); j++)
229-
{
230-
ref InputControlLayout.ControlItem controlLayoutItem = ref controlLayout.m_Controls[j];
231-
if (InputControlPath.MatchControlComponent(ref parsedPath[2], ref controlLayoutItem))
232-
{
233-
controlName += $"/{controlLayoutItem.name}";
234-
matchExists = true;
235-
}
236-
}
237-
}
238-
}
239167
else
240168
{
241-
matchExists = true;
169+
DrawMatchingControlPaths(matchingControlPaths);
242170
}
243171
}
244-
}
245-
246-
IEnumerable<InputControlLayout> matchedChildLayouts = EditorInputControlLayoutCache.TryGetChildLayouts(deviceLayout.name);
247172

248-
// If this layout does not have a match, or is the top level root layout,
249-
// skip over trying to draw any items for it, and immediately try processing the child layouts
250-
if (!matchExists)
251-
{
252-
foreach (var childLayout in matchedChildLayouts)
253-
{
254-
matchExists |= DrawMatchingControlPathsForLayout(childLayout, in parsedPath);
255-
}
256-
}
257-
// Otherwise, draw the items for it, and then only process the child layouts if the foldout is expanded.
258-
else
259-
{
260-
bool showLayout = false;
261-
EditorGUI.indentLevel++;
262-
if (matchedChildLayouts.Count() > 0 && !isRoot)
263-
{
264-
showMatchingChildLayouts.TryGetValue(deviceName, out showLayout);
265-
showMatchingChildLayouts[deviceName] = EditorGUILayout.Foldout(showLayout, $"{deviceName} > {controlName}");
266-
}
267-
else
268-
{
269-
EditorGUILayout.LabelField($"{deviceName} > {controlName}");
270-
}
271-
272-
showLayout |= isRoot;
273-
274-
if (showLayout)
275-
{
276-
foreach (var childLayout in matchedChildLayouts)
277-
{
278-
DrawMatchingControlPathsForLayout(childLayout, in parsedPath);
279-
}
280-
}
281-
EditorGUI.indentLevel--;
173+
EditorGUILayout.EndVertical();
282174
}
283-
284-
return matchExists;
285175
}
286176

287177
/// <summary>

Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsEditorStyles.uss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,17 @@
199199
.unity-two-pane-split-view__dragline-anchor {
200200
background-color: rgb(25, 25, 25);
201201
}
202+
203+
#control-scheme-usage-title {
204+
margin: 3px;
205+
-unity-font-style: bold;
206+
}
207+
208+
.matching-controls {
209+
display: flex;
210+
flex-grow: 1;
211+
}
212+
213+
.matching-controls-labels {
214+
margin: 1px;
215+
}

Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/BindingPropertiesView.cs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
using System.Linq;
33
using UnityEditor;
44
using UnityEngine.UIElements;
5+
using UnityEngine.InputSystem.Layouts;
6+
using UnityEngine.InputSystem.Utilities;
7+
using System.Collections.Generic;
58

69
namespace UnityEngine.InputSystem.Editor
710
{
@@ -50,6 +53,7 @@ public override void RedrawUI(ViewState viewState)
5053
else if (binding.Value.isPartOfComposite)
5154
{
5255
m_CompositePartBindingPropertiesView = CreateChildView(new CompositePartBindingPropertiesView(rootElement, stateContainer));
56+
DrawMatchingControlPaths(viewState);
5357
DrawControlSchemeToggles(viewState, binding.Value);
5458
}
5559
else
@@ -64,10 +68,77 @@ public override void RedrawUI(ViewState viewState)
6468
var controlPathContainer = new IMGUIContainer(controlPathEditor.OnGUI);
6569
rootElement.Add(controlPathContainer);
6670

71+
DrawMatchingControlPaths(viewState);
6772
DrawControlSchemeToggles(viewState, binding.Value);
6873
}
6974
}
7075

76+
static bool s_showMatchingLayouts = false;
77+
internal void DrawMatchingControlPaths(ViewState viewState)
78+
{
79+
bool controlPathUsagePresent = false;
80+
bool showPaths = s_showMatchingLayouts;
81+
List<MatchingControlPath> matchingControlPaths = MatchingControlPath.CollectMatchingControlPaths(viewState.selectedBindingPath.stringValue, showPaths, ref controlPathUsagePresent);
82+
83+
var parentElement = rootElement;
84+
if (matchingControlPaths == null || matchingControlPaths.Count != 0)
85+
{
86+
var controllingElement = new Foldout()
87+
{
88+
text = $"Show Derived Bindings",
89+
value = showPaths
90+
};
91+
rootElement.Add(controllingElement);
92+
93+
controllingElement.RegisterValueChangedCallback(changeEvent =>
94+
{
95+
if (changeEvent.target == controllingElement) // only react to foldout and not tree elements
96+
s_showMatchingLayouts = changeEvent.newValue;
97+
});
98+
99+
parentElement = controllingElement;
100+
}
101+
102+
if (matchingControlPaths == null)
103+
{
104+
var messageString = controlPathUsagePresent ? "No registered controls match this current binding. Some controls are only registered at runtime." :
105+
"No other registered controls match this current binding. Some controls are only registered at runtime.";
106+
107+
var helpBox = new HelpBox(messageString, HelpBoxMessageType.Warning);
108+
helpBox.AddToClassList("matching-controls");
109+
parentElement.Add(helpBox);
110+
}
111+
else if (matchingControlPaths.Count > 0)
112+
{
113+
List<TreeViewItemData<MatchingControlPath>> treeViewMatchingControlPaths = MatchingControlPath.BuildMatchingControlPathsTreeData(matchingControlPaths);
114+
115+
var treeView = new TreeView();
116+
parentElement.Add(treeView);
117+
treeView.selectionType = UIElements.SelectionType.None;
118+
treeView.AddToClassList("matching-controls");
119+
treeView.fixedItemHeight = 20;
120+
treeView.SetRootItems(treeViewMatchingControlPaths);
121+
122+
// Set TreeView.makeItem to initialize each node in the tree.
123+
treeView.makeItem = () =>
124+
{
125+
var label = new Label();
126+
label.AddToClassList("matching-controls-labels");
127+
return label;
128+
};
129+
130+
// Set TreeView.bindItem to bind an initialized node to a data item.
131+
treeView.bindItem = (VisualElement element, int index) =>
132+
{
133+
var label = (element as Label);
134+
var matchingControlPath = treeView.GetItemDataForIndex<MatchingControlPath>(index);
135+
label.text = $"{matchingControlPath.deviceName} > {matchingControlPath.controlName}";
136+
};
137+
138+
treeView.ExpandRootItems();
139+
}
140+
}
141+
71142
public override void DestroyView()
72143
{
73144
m_CompositeBindingPropertiesView?.DestroyView();
@@ -78,7 +149,11 @@ private void DrawControlSchemeToggles(ViewState viewState, SerializedInputBindin
78149
{
79150
if (!viewState.controlSchemes.Any()) return;
80151

81-
var useInControlSchemeLabel = new Label("Use in control scheme");
152+
var useInControlSchemeLabel = new Label("Use in control scheme")
153+
{
154+
name = "control-scheme-usage-title"
155+
};
156+
82157
rootElement.Add(useInControlSchemeLabel);
83158

84159
foreach (var controlScheme in viewState.controlSchemes)

0 commit comments

Comments
 (0)