Skip to content

Commit b92071e

Browse files
authored
FIX: Arrow key navigation of Input Actions after Action rename. (#2082)
1 parent 934f197 commit b92071e

File tree

5 files changed

+129
-23
lines changed

5 files changed

+129
-23
lines changed

Diff for: Assets/Tests/InputSystem.Editor/InputActionsEditorTests.cs

+65-1
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ public override void OneTimeSetUp()
2121
{
2222
base.OneTimeSetUp();
2323
m_Asset = AssetDatabaseUtils.CreateAsset<InputActionAsset>();
24-
m_Asset.AddActionMap("First Name");
24+
var actionMap = m_Asset.AddActionMap("First Name");
2525
m_Asset.AddActionMap("Second Name");
2626
m_Asset.AddActionMap("Third Name");
27+
28+
actionMap.AddAction("Action One");
29+
actionMap.AddAction("Action Two");
2730
}
2831

2932
public override void OneTimeTearDown()
@@ -55,6 +58,19 @@ IEnumerator WaitForActionMapRename(int index, bool isActive, double timeoutSecs
5558
}, $"WaitForActionMapRename {index} {isActive}", timeoutSecs);
5659
}
5760

61+
IEnumerator WaitForActionRename(int index, bool isActive, double timeoutSecs = 5.0)
62+
{
63+
return WaitUntil(() =>
64+
{
65+
var actionItems = m_Window.rootVisualElement.Q("actions-container").Query<InputActionsTreeViewItem>().ToList();
66+
if (actionItems.Count > index && actionItems[index].IsFocused == isActive)
67+
{
68+
return true;
69+
}
70+
return false;
71+
}, $"WaitForActionRename {index} {isActive}", timeoutSecs);
72+
}
73+
5874
#endregion
5975

6076
[Test]
@@ -177,5 +193,53 @@ public IEnumerator CanDeleteActionMap()
177193
Assert.That(m_Window.currentAssetInEditor.actionMaps[0].name, Is.EqualTo("First Name"));
178194
Assert.That(m_Window.currentAssetInEditor.actionMaps[1].name, Is.EqualTo("Third Name"));
179195
}
196+
197+
[UnityTest]
198+
[Ignore("Instability, see ISXB-1284")]
199+
public IEnumerator CanRenameAction()
200+
{
201+
var actionContainer = m_Window.rootVisualElement.Q("actions-container");
202+
var actionItem = actionContainer.Query<InputActionsTreeViewItem>().ToList();
203+
Assume.That(actionItem[1].Q<Label>("name").text, Is.EqualTo("Action Two"));
204+
205+
m_Window.rootVisualElement.Q<TreeView>("actions-tree-view").Focus();
206+
m_Window.rootVisualElement.Q<TreeView>("actions-tree-view").selectedIndex = 1;
207+
208+
// Selection change triggers a state change, wait for the scheduler to process the frame
209+
yield return WaitForSchedulerLoop();
210+
yield return WaitForNotDirty();
211+
yield return WaitForFocus(m_Window.rootVisualElement.Q<TreeView>("actions-tree-view"));
212+
213+
// Re-fetch the actions since the UI may have refreshed.
214+
actionItem = actionContainer.Query<InputActionsTreeViewItem>().ToList();
215+
216+
// Click twice to start the rename
217+
SimulateClickOn(actionItem[1]);
218+
// If the item is already focused, don't click again
219+
if (!actionItem[1].IsFocused)
220+
{
221+
SimulateClickOn(actionItem[1]);
222+
}
223+
224+
yield return WaitForActionRename(1, isActive: true);
225+
226+
// Rename the action
227+
SimulateTypingText("New Name");
228+
229+
// Wait for rename to end and focus to return from text field
230+
yield return WaitForSchedulerLoop();
231+
yield return WaitForFocus(m_Window.rootVisualElement.Q<TreeView>("actions-tree-view"));
232+
233+
// Check on the UI side
234+
actionContainer = m_Window.rootVisualElement.Q("actions-container");
235+
Assume.That(actionContainer, Is.Not.Null);
236+
actionItem = actionContainer.Query<InputActionsTreeViewItem>().ToList();
237+
Assert.That(actionItem, Is.Not.Null);
238+
Assert.That(actionItem.Count, Is.EqualTo(2));
239+
Assert.That(actionItem[1].Q<Label>("name").text, Is.EqualTo("New Name"));
240+
241+
// Check on the asset side
242+
Assert.That(m_Window.currentAssetInEditor.actionMaps[0].actions[1].name, Is.EqualTo("New Name"));
243+
}
180244
}
181245
#endif

Diff for: Packages/com.unity.inputsystem/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ however, it has to be formatted properly to pass verification tests.
1212

1313
### Fixed
1414
- Fixed an issue where removing a newly created action in the Asset Editor would cause an exception. [UUM-95693](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-95693)
15+
- Fixed arrow key navigation of Input Actions after Action rename. [ISXB-1024](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1024)
1516

1617
## [1.13.0] - 2025-02-05
1718

Diff for: Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/ActionsTreeView.cs

+16
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ public ActionsTreeView(VisualElement root, StateContainer stateContainer)
5858

5959
if (item.isAction)
6060
{
61+
// Items in the TreeView which were previously Bindings had input explicitly unregistered.
62+
// Since the input field is normally registered on creation, when using RefreshItem rather than Rebuild
63+
// it must be re-registered here.
64+
treeViewItem.RegisterInputField();
65+
6166
Action action = ContextMenu.GetContextMenuForActionAddItem(this, item.controlLayout, i);
6267
addBindingButton.clicked += action;
6368
addBindingButton.userData = action; // Store to use in unbindItem
@@ -76,6 +81,11 @@ public ActionsTreeView(VisualElement root, StateContainer stateContainer)
7681
treeViewItem.UnregisterInputField();
7782
else
7883
{
84+
// Items in the TreeView which were previously Bindings had input explicitly unregistered.
85+
// Since the input field is normally registered on creation, when using RefreshItem rather than Rebuild
86+
// it must be re-registered here.
87+
treeViewItem.RegisterInputField();
88+
7989
treeViewItem.EditTextFinishedCallback = newName =>
8090
{
8191
ChangeActionOrCompositName(item, newName);
@@ -205,7 +215,13 @@ public override void RedrawUI(ViewState viewState)
205215
{
206216
m_ActionsTreeView.Clear();
207217
m_ActionsTreeView.SetRootItems(viewState.treeViewData);
218+
// UI toolkit doesn't behave the same on 6000.0 way when refreshing items
219+
// On previous versions, we need to call Rebuild() to refresh the items since refreshItems() is less predicatable
220+
#if UNITY_6000_0_OR_NEWER
221+
m_ActionsTreeView.RefreshItems();
222+
#else
208223
m_ActionsTreeView.Rebuild();
224+
#endif
209225
if (viewState.newElementID != -1)
210226
{
211227
m_ActionsTreeView.SetSelectionById(viewState.newElementID);

Diff for: Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/ContextMenu.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,14 @@ public static void GetContextMenuForActionsEmptySpace(ActionsTreeView actionsTre
151151

152152
public static void GetContextMenuForActionItem(ActionsTreeView treeView, InputActionsTreeViewItem treeViewItem, string controlLayout, int index)
153153
{
154-
_ = new ContextualMenuManipulator(menuEvent =>
154+
treeViewItem.OnContextualMenuPopulateEvent = (menuEvent =>
155155
{
156156
menuEvent.menu.AppendAction(add_Binding_String, _ => treeView.AddBinding(index));
157157
AppendCompositeMenuItems(treeView, controlLayout, index, (name, action) => menuEvent.menu.AppendAction(name, _ => action.Invoke()));
158158
menuEvent.menu.AppendSeparator();
159159
AppendRenameAction(menuEvent, treeView, index);
160160
AppendDuplicateDeleteCutAndCopyActionsSection(menuEvent, treeView, index);
161-
}) { target = treeViewItem };
161+
});
162162
}
163163

164164
public static Action GetContextMenuForActionAddItem(ActionsTreeView treeView, string controlLayout, int index)
@@ -202,19 +202,19 @@ private static void AppendCompositeMenuItems(ActionsTreeView treeView, string ex
202202

203203
public static void GetContextMenuForCompositeItem(ActionsTreeView treeView, InputActionsTreeViewItem treeViewItem, int index)
204204
{
205-
_ = new ContextualMenuManipulator(menuEvent =>
205+
treeViewItem.OnContextualMenuPopulateEvent = (menuEvent =>
206206
{
207207
AppendRenameAction(menuEvent, treeView, index);
208208
AppendDuplicateDeleteCutAndCopyActionsSection(menuEvent, treeView, index);
209-
}) { target = treeViewItem };
209+
});
210210
}
211211

212212
public static void GetContextMenuForBindingItem(ActionsTreeView treeView, InputActionsTreeViewItem treeViewItem, int index)
213213
{
214-
_ = new ContextualMenuManipulator(menuEvent =>
214+
treeViewItem.OnContextualMenuPopulateEvent = (menuEvent =>
215215
{
216216
AppendDuplicateDeleteCutAndCopyActionsSection(menuEvent, treeView, index);
217-
}) { target = treeViewItem };
217+
});
218218
}
219219

220220
private static void AppendRenameAction(ContextualMenuPopulateEvent menuEvent, ActionsTreeView treeView, int index)

Diff for: Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Views/InputActionsTreeViewItem.cs

+41-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// UITK TreeView is not supported in earlier versions
22
// Therefore the UITK version of the InputActionAsset Editor is not available on earlier Editor versions either.
33
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
4+
using System;
45
using System.Threading.Tasks;
56
using UnityEditor;
67
using UnityEngine.UIElements;
@@ -16,6 +17,10 @@ internal class InputActionsTreeViewItem : VisualElement
1617

1718
private const string kRenameTextField = "rename-text-field";
1819
public event EventCallback<string> EditTextFinished;
20+
public Action<ContextualMenuPopulateEvent> OnContextualMenuPopulateEvent;
21+
22+
// for testing purposes to know if the item is focused to accept input
23+
internal bool IsFocused { get; private set; } = false;
1924

2025
private bool m_IsEditing;
2126
private static InputActionsTreeViewItem s_EditingItem = null;
@@ -33,17 +38,32 @@ public InputActionsTreeViewItem()
3338
focusable = true;
3439
delegatesFocus = false;
3540

36-
renameTextfield.selectAllOnFocus = true;
3741
renameTextfield.selectAllOnMouseUp = false;
3842

39-
40-
RegisterCallback<MouseDownEvent>(OnMouseDownEventForRename);
41-
renameTextfield.RegisterCallback<FocusOutEvent>(e => OnEditTextFinished());
43+
RegisterInputField();
44+
_ = new ContextualMenuManipulator(menuBuilder =>
45+
{
46+
OnContextualMenuPopulateEvent?.Invoke(menuBuilder);
47+
})
48+
{ target = this };
4249
}
4350

4451
public Label label => this.Q<Label>();
4552
private TextField renameTextfield => this.Q<TextField>(kRenameTextField);
4653

54+
public void RegisterInputField()
55+
{
56+
renameTextfield.SetEnabled(true);
57+
renameTextfield.selectAllOnFocus = true;
58+
RegisterCallback<MouseDownEvent>(OnMouseDownEventForRename);
59+
renameTextfield.RegisterCallback<FocusInEvent>(e => IsFocused = true);
60+
renameTextfield.RegisterCallback<FocusOutEvent>(e =>
61+
{
62+
OnEditTextFinished();
63+
IsFocused = false;
64+
});
65+
}
66+
4767
public void UnregisterInputField()
4868
{
4969
renameTextfield.SetEnabled(false);
@@ -52,28 +72,38 @@ public void UnregisterInputField()
5272
renameTextfield.UnregisterCallback<FocusOutEvent>(e => OnEditTextFinished());
5373
}
5474

55-
private float lastSingleClick;
75+
private double lastSingleClick;
5676
private static InputActionsTreeViewItem selected;
5777

5878
private void OnMouseDownEventForRename(MouseDownEvent e)
5979
{
6080
if (e.clickCount != 1 || e.button != (int)MouseButton.LeftMouse || e.target == null)
6181
return;
62-
63-
if (selected == this && Time.time - lastSingleClick < 3f)
82+
var now = EditorApplication.timeSinceStartup;
83+
if (selected == this && now - lastSingleClick < 3)
6484
{
6585
FocusOnRenameTextField();
6686
e.StopImmediatePropagation();
6787
lastSingleClick = 0;
88+
return;
6889
}
69-
lastSingleClick = Time.time;
90+
lastSingleClick = now;
7091
selected = this;
7192
}
7293

7394
public void Reset()
7495
{
96+
if (m_IsEditing)
97+
{
98+
lastSingleClick = 0;
99+
delegatesFocus = false;
100+
101+
renameTextfield.AddToClassList(InputActionsEditorConstants.HiddenStyleClassName);
102+
label.RemoveFromClassList(InputActionsEditorConstants.HiddenStyleClassName);
103+
s_EditingItem = null;
104+
m_IsEditing = false;
105+
}
75106
EditTextFinished = null;
76-
m_IsEditing = false;
77107
}
78108

79109
public void FocusOnRenameTextField()
@@ -88,7 +118,7 @@ public void FocusOnRenameTextField()
88118

89119
//a bit hacky - e.StopImmediatePropagation() for events does not work like expected on ListViewItems or TreeViewItems because
90120
//the listView/treeView reclaims the focus - this is a workaround with less overhead than rewriting the events
91-
DelayCall();
121+
schedule.Execute(() => renameTextfield.Q<TextField>().Focus()).StartingIn(120);
92122
renameTextfield.SelectAll();
93123

94124
s_EditingItem = this;
@@ -100,12 +130,6 @@ public static void CancelRename()
100130
s_EditingItem?.OnEditTextFinished();
101131
}
102132

103-
async void DelayCall()
104-
{
105-
await Task.Delay(120);
106-
renameTextfield.Q<TextField>().Focus();
107-
}
108-
109133
private void OnEditTextFinished()
110134
{
111135
if (!m_IsEditing)
@@ -124,6 +148,7 @@ private void OnEditTextFinished()
124148
renameTextfield.schedule.Execute(() => renameTextfield.SetValueWithoutNotify(text));
125149
return;
126150
}
151+
127152
EditTextFinished?.Invoke(text);
128153
}
129154
}

0 commit comments

Comments
 (0)