A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
2using System;
3using System.Collections.Generic;
4using System.Linq;
5using UnityEditor;
6using UnityEngine.UIElements;
7using PopupWindow = UnityEngine.UIElements.PopupWindow;
8
9namespace UnityEngine.InputSystem.Editor
10{
11 internal class ControlSchemesView : ViewBase<InputControlScheme>
12 {
13 //is used to save the new name of the control scheme when renaming
14 private string m_NewName;
15 public event Action<ViewBase<InputControlScheme>> OnClosing;
16
17 public ControlSchemesView(VisualElement root, StateContainer stateContainer, bool updateExisting = false)
18 : base(root, stateContainer)
19 {
20 m_UpdateExisting = updateExisting;
21
22 var controlSchemeEditor = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
23 InputActionsEditorConstants.PackagePath +
24 InputActionsEditorConstants.ResourcesPath +
25 InputActionsEditorConstants.ControlSchemeEditorViewUxml);
26
27 var controlSchemeVisualElement = controlSchemeEditor.CloneTree();
28 controlSchemeVisualElement.Q<Button>(kCancelButton).clicked += Cancel;
29 controlSchemeVisualElement.Q<Button>(kSaveButton).clicked += SaveAndClose;
30 controlSchemeVisualElement.Q<TextField>(kControlSchemeNameTextField).RegisterCallback<FocusOutEvent>(evt =>
31 {
32 Dispatch((in InputActionsEditorState state) =>
33 {
34 // If the name is the same as the current name, don't change it
35 var newName = ((TextField)evt.currentTarget).value.Trim();
36 if (string.IsNullOrEmpty(newName) || String.Compare(newName, state.selectedControlScheme.name) == 0)
37 {
38 m_NewName = String.Empty;
39 // write back the value to the text field if the name was empty
40 ((TextField)evt.currentTarget).value = state.selectedControlScheme.name;
41 }
42 else
43 {
44 m_NewName = ControlSchemeCommands.MakeUniqueControlSchemeName(state, newName);
45 // write back the value to the text field if the name was not unique
46 ((TextField)evt.currentTarget).value = m_NewName;
47 }
48
49 return state.With(selectedControlScheme: state.selectedControlScheme);
50 });
51 });
52
53 m_ModalWindow = new VisualElement
54 {
55 style = { position = new StyleEnum<Position>(Position.Absolute) }
56 };
57 var popupWindow = new PopupWindow
58 {
59 text = "Add Control Scheme",
60 style = { position = new StyleEnum<Position>(Position.Absolute) }
61 };
62 popupWindow.contentContainer.Add(controlSchemeVisualElement);
63 m_ModalWindow.Add(popupWindow);
64 root.Add(m_ModalWindow);
65 m_ModalWindow.StretchToParentSize();
66 m_ModalWindow.RegisterCallback<ClickEvent>(evt => CloseView());
67 popupWindow.RegisterCallback<ClickEvent>(evt => evt.StopPropagation());
68
69 m_ListView = controlSchemeVisualElement.Q<MultiColumnListView>(kControlSchemesListView);
70 m_ListView.columns[kDeviceTypeColumnName].makeCell = () => new Label();
71 m_ListView.columns[kDeviceTypeColumnName].bindCell = BindDeviceTypeCell;
72
73 m_ListView.columns[kRequiredColumnName].makeCell = MakeRequiredCell;
74 m_ListView.columns[kRequiredColumnName].bindCell = BindDeviceRequiredCell;
75 m_ListView.columns[kRequiredColumnName].unbindCell = UnbindDeviceRequiredCell;
76
77 m_ListView.Q<Button>(kUnityListViewAddButton).clickable = new Clickable(AddDeviceRequirement);
78 m_ListView.Q<Button>(kUnityListViewRemoveButton).clickable = new Clickable(RemoveDeviceRequirement);
79
80 m_ListView.itemIndexChanged += (oldPosition, newPosition) =>
81 {
82 Dispatch(ControlSchemeCommands.ReorderDeviceRequirements(oldPosition, newPosition));
83 };
84
85 m_ListView.itemsSource = new List<string>();
86
87 CreateSelector(s => s.selectedControlScheme,
88 (_, s) => s.selectedControlScheme);
89 }
90
91 private void AddDeviceRequirement()
92 {
93 var dropdown = new InputControlPickerDropdown(new InputControlPickerState(), path =>
94 {
95 var requirement = new InputControlScheme.DeviceRequirement { controlPath = path, isOptional = false };
96 Dispatch(ControlSchemeCommands.AddDeviceRequirement(requirement));
97 }, mode: InputControlPicker.Mode.PickDevice);
98 dropdown.Show(new Rect(Event.current.mousePosition, Vector2.zero));
99 }
100
101 private void RemoveDeviceRequirement()
102 {
103 if (m_ListView.selectedIndex == -1)
104 return;
105
106 Dispatch(ControlSchemeCommands.RemoveDeviceRequirement(m_ListView.selectedIndex));
107 }
108
109 public override void RedrawUI(InputControlScheme viewState)
110 {
111 rootElement.Q<TextField>(kControlSchemeNameTextField).value = string.IsNullOrEmpty(m_NewName) ? viewState.name : m_NewName;
112
113 m_ListView.itemsSource?.Clear();
114 m_ListView.itemsSource = viewState.deviceRequirements.Count > 0 ?
115 viewState.deviceRequirements.Select(r => (r.controlPath, r.isOptional)).ToList() :
116 new List<(string, bool)>();
117 m_ListView.Rebuild();
118 }
119
120 public override void DestroyView()
121 {
122 m_ModalWindow.RemoveFromHierarchy();
123 }
124
125 private void SaveAndClose()
126 {
127 // Persist the current ControlScheme values to the SerializedProperty
128 Dispatch(ControlSchemeCommands.SaveControlScheme(m_NewName, m_UpdateExisting));
129 CloseView();
130 }
131
132 private void Cancel()
133 {
134 // Reload the selected ControlScheme values from the SerilaizedProperty and throw away any changes
135 Dispatch(ControlSchemeCommands.ResetSelectedControlScheme());
136 CloseView();
137 }
138
139 private void CloseView()
140 {
141 // Closing the View without explicitly selecting "Save" or "Cancel" holds the values in the
142 // current UI state but won't persist them; the Asset Editor state isn't dirtied.
143 //
144 // This means accidentally clicking outside of the View (closes the dialog) won't immediately
145 // cause loss of work: the "Edit Control Scheme" option can be selected to re-open the View
146 // the changes retained. However, if a different ControlScheme is selected or the Asset
147 // Editor window is closed, then the changes are lost.
148
149 m_NewName = string.Empty;
150 OnClosing?.Invoke(this);
151 }
152
153 private VisualElement MakeRequiredCell()
154 {
155 var ve = new VisualElement
156 {
157 style =
158 {
159 flexDirection = FlexDirection.Column,
160 flexGrow = 1,
161 alignContent = new StyleEnum<Align>(Align.Center)
162 }
163 };
164 ve.Add(new Toggle());
165 return ve;
166 }
167
168 private void BindDeviceRequiredCell(VisualElement visualElement, int rowIndex)
169 {
170 var toggle = visualElement.Q<Toggle>();
171 var rowItem = ((string path, bool optional))m_ListView.itemsSource[rowIndex];
172
173 toggle.value = !rowItem.optional;
174 var eventCallback = (EventCallback<ChangeEvent<bool>>)(evt =>
175 Dispatch(ControlSchemeCommands.ChangeDeviceRequirement(rowIndex, evt.newValue)));
176 toggle.userData = eventCallback;
177
178 toggle.RegisterValueChangedCallback(eventCallback);
179 }
180
181 private void UnbindDeviceRequiredCell(VisualElement visualElement, int rowIndex)
182 {
183 var toggle = visualElement.Q<Toggle>();
184 toggle.UnregisterValueChangedCallback((EventCallback<ChangeEvent<bool>>)toggle.userData);
185 }
186
187 private void BindDeviceTypeCell(VisualElement visualElement, int rowIndex)
188 {
189 ((Label)visualElement).text = (((string, bool))m_ListView.itemsSource[rowIndex]).Item1;
190 }
191
192 private readonly bool m_UpdateExisting;
193 private MultiColumnListView m_ListView;
194 private VisualElement m_ModalWindow;
195
196 private const string kControlSchemeNameTextField = "control-scheme-name";
197 private const string kCancelButton = "cancel-button";
198 private const string kSaveButton = "save-button";
199 private const string kControlSchemesListView = "control-schemes-list-view";
200 private const string kDeviceTypeColumnName = "device-type";
201 private const string kRequiredColumnName = "required";
202 private const string kUnityListViewAddButton = "unity-list-view__add-button";
203 private const string kUnityListViewRemoveButton = "unity-list-view__remove-button";
204 }
205}
206
207#endif