#if UNITY_EDITOR using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.IMGUI.Controls; using UnityEditorInternal; using UnityEngine.InputSystem.Utilities; ////TODO: better method for creating display names than InputControlPath.TryGetDeviceLayout ////FIXME: Device requirements list in control scheme popup must mention explicitly that that is what it is namespace UnityEngine.InputSystem.Editor { /// /// Toolbar in input action asset editor. /// /// /// Allows editing and selecting from the set of control schemes as well as selecting from the /// set of device requirements within the currently selected control scheme. /// /// Also controls saving and has the global search text field. /// /// [Serializable] internal class InputActionEditorToolbar { public void OnGUI() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); DrawSchemeSelection(); DrawDeviceFilterSelection(); if (!InputEditorUserSettings.autoSaveInputActionAssets) DrawSaveButton(); GUILayout.FlexibleSpace(); DrawAutoSaveToggle(); GUILayout.Space(5); DrawSearchField(); GUILayout.Space(5); EditorGUILayout.EndHorizontal(); } private void DrawSchemeSelection() { var buttonGUI = m_ControlSchemes.LengthSafe() > 0 ? new GUIContent(selectedControlScheme?.name ?? "All Control Schemes") : new GUIContent("No Control Schemes"); var buttonRect = GUILayoutUtility.GetRect(buttonGUI, EditorStyles.toolbarPopup, GUILayout.MinWidth(k_MinimumButtonWidth)); if (GUI.Button(buttonRect, buttonGUI, EditorStyles.toolbarPopup)) { // PopupWindow.Show already takes the current OnGUI EditorWindow context into account for window coordinates. // However, on macOS, menu commands are performed asynchronously, so we don't have a current OnGUI context. // So in that case, we need to translate the rect to screen coordinates. Don't do that on windows, as we will // overcompensate otherwise. if (Application.platform == RuntimePlatform.OSXEditor) buttonRect = new Rect(EditorGUIUtility.GUIToScreenPoint(new Vector2(buttonRect.x, buttonRect.y)), Vector2.zero); var menu = new GenericMenu(); // Add entries to select control scheme, if we have some. if (m_ControlSchemes.LengthSafe() > 0) { menu.AddItem(s_AllControlSchemes, m_SelectedControlSchemeIndex == -1, OnControlSchemeSelected, null); var selectedControlSchemeName = m_SelectedControlSchemeIndex == -1 ? null : m_ControlSchemes[m_SelectedControlSchemeIndex].name; foreach (var controlScheme in m_ControlSchemes.OrderBy(x => x.name)) menu.AddItem(new GUIContent(controlScheme.name), controlScheme.name == selectedControlSchemeName, OnControlSchemeSelected, controlScheme.name); menu.AddSeparator(string.Empty); } // Add entries to add/edit/duplicate/delete control schemes. menu.AddItem(s_AddControlSchemeLabel, false, OnAddControlScheme, buttonRect); if (m_SelectedControlSchemeIndex >= 0) { menu.AddItem(s_EditControlSchemeLabel, false, OnEditSelectedControlScheme, buttonRect); menu.AddItem(s_DuplicateControlSchemeLabel, false, OnDuplicateControlScheme, buttonRect); menu.AddItem(s_DeleteControlSchemeLabel, false, OnDeleteControlScheme); } else { menu.AddDisabledItem(s_EditControlSchemeLabel, false); menu.AddDisabledItem(s_DuplicateControlSchemeLabel, false); menu.AddDisabledItem(s_DeleteControlSchemeLabel, false); } menu.ShowAsContext(); } } private void DrawDeviceFilterSelection() { // Lazy-initialize list of GUIContents that represent each individual device requirement. if (m_SelectedSchemeDeviceRequirementNames == null && m_ControlSchemes.LengthSafe() > 0 && m_SelectedControlSchemeIndex >= 0) { m_SelectedSchemeDeviceRequirementNames = m_ControlSchemes[m_SelectedControlSchemeIndex] .deviceRequirements.Select(x => new GUIContent(DeviceRequirementToDisplayString(x))) .ToArray(); } EditorGUI.BeginDisabledGroup(m_SelectedControlSchemeIndex < 0); if (m_SelectedSchemeDeviceRequirementNames.LengthSafe() == 0) { GUILayout.Button(s_AllDevicesLabel, EditorStyles.toolbarPopup, GUILayout.MinWidth(k_MinimumButtonWidth)); } else if (GUILayout.Button(m_SelectedDeviceRequirementIndex < 0 ? s_AllDevicesLabel : m_SelectedSchemeDeviceRequirementNames[m_SelectedDeviceRequirementIndex], EditorStyles.toolbarPopup, GUILayout.MinWidth(k_MinimumButtonWidth))) { var menu = new GenericMenu(); menu.AddItem(s_AllDevicesLabel, m_SelectedControlSchemeIndex == -1, OnSelectedDeviceChanged, -1); for (var i = 0; i < m_SelectedSchemeDeviceRequirementNames.Length; i++) menu.AddItem(m_SelectedSchemeDeviceRequirementNames[i], m_SelectedDeviceRequirementIndex == i, OnSelectedDeviceChanged, i); menu.ShowAsContext(); } EditorGUI.EndDisabledGroup(); } private void DrawSaveButton() { EditorGUI.BeginDisabledGroup(!m_IsDirty); EditorGUILayout.Space(); if (GUILayout.Button(s_SaveAssetLabel, EditorStyles.toolbarButton)) onSave(); EditorGUI.EndDisabledGroup(); } private void DrawAutoSaveToggle() { ////FIXME: Using a normal Toggle style with a miniFont, I can't get the "Auto-Save" label to align properly on the vertical. //// The workaround here splits it into a toggle with an empty label plus an extra label. //// Not using EditorStyles.toolbarButton here as it makes it hard to tell that it's a toggle. if (s_MiniToggleStyle == null) { s_MiniToggleStyle = new GUIStyle("Toggle") { font = EditorStyles.miniFont, margin = new RectOffset(0, 0, 1, 0), padding = new RectOffset(0, 16, 0, 0) }; s_MiniLabelStyle = new GUIStyle("Label") { font = EditorStyles.miniFont, margin = new RectOffset(0, 0, 3, 0) }; } var autoSaveNew = GUILayout.Toggle(InputEditorUserSettings.autoSaveInputActionAssets, "", s_MiniToggleStyle); GUILayout.Label(s_AutoSaveLabel, s_MiniLabelStyle); if (autoSaveNew != InputEditorUserSettings.autoSaveInputActionAssets && autoSaveNew && m_IsDirty) { // If it changed from disabled to enabled, perform an initial save. onSave(); } InputEditorUserSettings.autoSaveInputActionAssets = autoSaveNew; GUILayout.Space(5); } private void DrawSearchField() { if (m_SearchField == null) m_SearchField = new SearchField(); EditorGUI.BeginChangeCheck(); m_SearchText = m_SearchField.OnToolbarGUI(m_SearchText, GUILayout.MaxWidth(250)); if (EditorGUI.EndChangeCheck()) onSearchChanged?.Invoke(); } private void OnControlSchemeSelected(object nameObj) { var index = -1; var name = (string)nameObj; if (name != null) { index = ArrayHelpers.IndexOf(m_ControlSchemes, x => x.name.Equals(name, StringComparison.InvariantCultureIgnoreCase)); Debug.Assert(index != -1, $"Cannot find control scheme {name}"); } m_SelectedControlSchemeIndex = index; m_SelectedDeviceRequirementIndex = -1; m_SelectedSchemeDeviceRequirementNames = null; onSelectedSchemeChanged?.Invoke(); } private void OnSelectedDeviceChanged(object indexObj) { Debug.Assert(m_SelectedControlSchemeIndex >= 0, "Control scheme must be selected"); m_SelectedDeviceRequirementIndex = (int)indexObj; onSelectedDeviceChanged?.Invoke(); } private void OnAddControlScheme(object position) { var uniqueName = MakeUniqueControlSchemeName("New control scheme"); ControlSchemePropertiesPopup.Show((Rect)position, new InputControlScheme(uniqueName), (s, _) => AddAndSelectControlScheme(s)); } private void OnDeleteControlScheme() { Debug.Assert(m_SelectedControlSchemeIndex >= 0, "Control scheme must be selected"); var name = m_ControlSchemes[m_SelectedControlSchemeIndex].name; var bindingGroup = m_ControlSchemes[m_SelectedControlSchemeIndex].bindingGroup; // Ask for confirmation. if (!EditorUtility.DisplayDialog("Delete scheme?", $"Do you want to delete control scheme '{name}'?", "Delete", "Cancel")) return; ArrayHelpers.EraseAt(ref m_ControlSchemes, m_SelectedControlSchemeIndex); m_SelectedControlSchemeIndex = -1; m_SelectedSchemeDeviceRequirementNames = null; if (m_SelectedDeviceRequirementIndex >= 0) { m_SelectedDeviceRequirementIndex = -1; onSelectedDeviceChanged?.Invoke(); } onControlSchemesChanged?.Invoke(); onSelectedSchemeChanged?.Invoke(); onControlSchemeDeleted?.Invoke(name, bindingGroup); } ////REVIEW: this does nothing to bindings; should this ask to duplicate bindings as well? private void OnDuplicateControlScheme(object position) { Debug.Assert(m_SelectedControlSchemeIndex >= 0, "Control scheme must be selected"); var scheme = m_ControlSchemes[m_SelectedControlSchemeIndex]; scheme = new InputControlScheme(MakeUniqueControlSchemeName(scheme.name), devices: scheme.deviceRequirements); ControlSchemePropertiesPopup.Show((Rect)position, scheme, (s, _) => AddAndSelectControlScheme(s)); } private void OnEditSelectedControlScheme(object position) { Debug.Assert(m_SelectedControlSchemeIndex >= 0, "Control scheme must be selected"); ControlSchemePropertiesPopup.Show((Rect)position, m_ControlSchemes[m_SelectedControlSchemeIndex], UpdateControlScheme, m_SelectedControlSchemeIndex); } private void AddAndSelectControlScheme(InputControlScheme scheme) { // Ensure scheme has a name. if (string.IsNullOrEmpty(scheme.name)) scheme.m_Name = "New control scheme"; // Make sure name is unique. scheme.m_Name = MakeUniqueControlSchemeName(scheme.name); var index = ArrayHelpers.Append(ref m_ControlSchemes, scheme); onControlSchemesChanged?.Invoke(); SelectControlScheme(index); } private void UpdateControlScheme(InputControlScheme scheme, int index) { Debug.Assert(index >= 0 && index < m_ControlSchemes.LengthSafe(), "Control scheme index out of range"); var renamed = false; string oldBindingGroup = null; string newBindingGroup = null; // If given scheme has no name, preserve the existing one on the control scheme. if (string.IsNullOrEmpty(scheme.name)) scheme.m_Name = m_ControlSchemes[index].name; // If name is changing, make sure it's unique. else if (scheme.name != m_ControlSchemes[index].name) { renamed = true; oldBindingGroup = m_ControlSchemes[index].bindingGroup; m_ControlSchemes[index].m_Name = ""; // Don't want this to interfere with finding a unique name. var newName = MakeUniqueControlSchemeName(scheme.name); m_ControlSchemes[index].SetNameAndBindingGroup(newName); newBindingGroup = m_ControlSchemes[index].bindingGroup; } m_ControlSchemes[index] = scheme; onControlSchemesChanged?.Invoke(); if (renamed) onControlSchemeRenamed?.Invoke(oldBindingGroup, newBindingGroup); } private void SelectControlScheme(int index) { Debug.Assert(index >= 0 && index < m_ControlSchemes.LengthSafe(), "Control scheme index out of range"); m_SelectedControlSchemeIndex = index; m_SelectedSchemeDeviceRequirementNames = null; onSelectedSchemeChanged?.Invoke(); // Reset device selection. if (m_SelectedDeviceRequirementIndex != -1) { m_SelectedDeviceRequirementIndex = -1; onSelectedDeviceChanged?.Invoke(); } } private string MakeUniqueControlSchemeName(string name) { const string presetName = "All Control Schemes"; if (m_ControlSchemes == null) return StringHelpers.MakeUniqueName(name, new[] {presetName}, x => x); return StringHelpers.MakeUniqueName(name, m_ControlSchemes.Select(x => x.name).Append(presetName), x => x); } private static string DeviceRequirementToDisplayString(InputControlScheme.DeviceRequirement requirement) { ////TODO: need something more flexible to produce correct results for more than the simple string we produce here var deviceLayout = InputControlPath.TryGetDeviceLayout(requirement.controlPath); var deviceLayoutText = !string.IsNullOrEmpty(deviceLayout) ? EditorInputControlLayoutCache.GetDisplayName(deviceLayout) : string.Empty; var usages = InputControlPath.TryGetDeviceUsages(requirement.controlPath); if (usages != null && usages.Length > 0) return $"{deviceLayoutText} {string.Join("}{", usages)}"; return deviceLayoutText; } // Notifications. public Action onSearchChanged; public Action onSelectedSchemeChanged; public Action onSelectedDeviceChanged; public Action onControlSchemesChanged; public Action onControlSchemeRenamed; public Action onControlSchemeDeleted; public Action onSave; [SerializeField] private bool m_IsDirty; [SerializeField] private int m_SelectedControlSchemeIndex = -1; [SerializeField] private int m_SelectedDeviceRequirementIndex = -1; [SerializeField] private InputControlScheme[] m_ControlSchemes; [SerializeField] private string m_SearchText; private GUIContent[] m_SelectedSchemeDeviceRequirementNames; private SearchField m_SearchField; private static readonly GUIContent s_AllControlSchemes = EditorGUIUtility.TrTextContent("All Control Schemes"); private static readonly GUIContent s_AddControlSchemeLabel = new GUIContent("Add Control Scheme..."); private static readonly GUIContent s_EditControlSchemeLabel = EditorGUIUtility.TrTextContent("Edit Control Scheme..."); private static readonly GUIContent s_DuplicateControlSchemeLabel = EditorGUIUtility.TrTextContent("Duplicate Control Scheme..."); private static readonly GUIContent s_DeleteControlSchemeLabel = EditorGUIUtility.TrTextContent("Delete Control Scheme..."); private static readonly GUIContent s_SaveAssetLabel = EditorGUIUtility.TrTextContent("Save Asset"); private static readonly GUIContent s_AutoSaveLabel = EditorGUIUtility.TrTextContent("Auto-Save"); private static readonly GUIContent s_AllDevicesLabel = EditorGUIUtility.TrTextContent("All Devices"); private static GUIStyle s_MiniToggleStyle; private static GUIStyle s_MiniLabelStyle; private const float k_MinimumButtonWidth = 110f; public ReadOnlyArray controlSchemes { get => m_ControlSchemes; set { m_ControlSchemes = value.ToArray(); m_SelectedSchemeDeviceRequirementNames = null; } } /// /// The control scheme currently selected in the toolbar or null if none is selected. /// public InputControlScheme? selectedControlScheme => m_SelectedControlSchemeIndex >= 0 ? new InputControlScheme ? (m_ControlSchemes[m_SelectedControlSchemeIndex]) : null; /// /// The device requirement of the currently selected control scheme which is currently selected /// in the toolbar or null if none is selected. /// public InputControlScheme.DeviceRequirement? selectedDeviceRequirement => m_SelectedDeviceRequirementIndex >= 0 ? new InputControlScheme.DeviceRequirement ? (m_ControlSchemes[m_SelectedControlSchemeIndex] .deviceRequirements[m_SelectedDeviceRequirementIndex]) : null; /// /// The search text currently entered in the toolbar or null. /// public string searchText => m_SearchText; internal void ResetSearchFilters() { m_SearchText = null; m_SelectedControlSchemeIndex = -1; m_SelectedDeviceRequirementIndex = -1; } public bool isDirty { get => m_IsDirty; set => m_IsDirty = value; } /// /// Popup window content for editing control schemes. /// private class ControlSchemePropertiesPopup : PopupWindowContent { public static void Show(Rect position, InputControlScheme controlScheme, Action onApply, int controlSchemeIndex = -1) { var popup = new ControlSchemePropertiesPopup { m_ControlSchemeIndex = controlSchemeIndex, m_ControlScheme = controlScheme, m_OnApply = onApply, m_SetFocus = true, }; // We're calling here from a callback, so we need to manually handle ExitGUIException. try { PopupWindow.Show(position, popup); } catch (ExitGUIException) {} } public override Vector2 GetWindowSize() { return m_ButtonsAndLabelsHeights > 0 ? new Vector2(300, m_ButtonsAndLabelsHeights) : s_DefaultSize; } public override void OnOpen() { m_DeviceList = m_ControlScheme.deviceRequirements.Select(a => new DeviceEntry(a)).ToList(); m_DeviceView = new ReorderableList(m_DeviceList, typeof(InputControlScheme.DeviceRequirement)); m_DeviceView.headerHeight = 2; m_DeviceView.onAddCallback += list => { var dropdown = new InputControlPickerDropdown( new InputControlPickerState(), path => { var requirement = new InputControlScheme.DeviceRequirement { controlPath = path, isOptional = false }; AddDeviceRequirement(requirement); }, mode: InputControlPicker.Mode.PickDevice); dropdown.Show(new Rect(Event.current.mousePosition, Vector2.zero)); }; m_DeviceView.onRemoveCallback += list => { list.list.RemoveAt(list.index); list.index = -1; }; } public override void OnGUI(Rect rect) { if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Escape) { editorWindow.Close(); Event.current.Use(); } if (Event.current.type == EventType.Repaint) m_ButtonsAndLabelsHeights = 0; GUILayout.BeginArea(rect); DrawTopBar(); EditorGUILayout.BeginVertical(EditorStyles.label); DrawSpace(); DrawNameEditTextField(); DrawSpace(); DrawDeviceList(); DrawConfirmationButton(); EditorGUILayout.EndVertical(); GUILayout.EndArea(); } private void DrawConfirmationButton() { EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Cancel", GUILayout.ExpandWidth(true))) { editorWindow.Close(); } if (GUILayout.Button("Save", GUILayout.ExpandWidth(true))) { // Don't allow control scheme name to be empty. if (string.IsNullOrEmpty(m_ControlScheme.name)) { ////FIXME: On 2019.1 this doesn't display properly in the window; check 2019.3 editorWindow.ShowNotification(new GUIContent("Control scheme must have a name")); } else { m_ControlScheme = new InputControlScheme(m_ControlScheme.name, devices: m_DeviceList.Select(a => a.deviceRequirement)); editorWindow.Close(); m_OnApply(m_ControlScheme, m_ControlSchemeIndex); } } if (Event.current.type == EventType.Repaint) m_ButtonsAndLabelsHeights += GUILayoutUtility.GetLastRect().height; EditorGUILayout.EndHorizontal(); } private void DrawDeviceList() { EditorGUILayout.BeginHorizontal(EditorStyles.label); var requirementsLabelSize = EditorStyles.label.CalcSize(s_RequirementsLabel); var deviceListRect = GUILayoutUtility.GetRect(GetWindowSize().x - requirementsLabelSize.x - 20, m_DeviceView.GetHeight()); m_DeviceView.DoList(deviceListRect); var requirementsHeight = DrawRequirementsCheckboxes(); var listHeight = m_DeviceView.GetHeight() + EditorGUIUtility.singleLineHeight * 3; if (Event.current.type == EventType.Repaint) { if (listHeight < requirementsHeight) { m_ButtonsAndLabelsHeights += requirementsHeight; } else { m_ButtonsAndLabelsHeights += listHeight; } } EditorGUILayout.EndHorizontal(); } private void DrawSpace() { GUILayout.Space(6f); if (Event.current.type == EventType.Repaint) m_ButtonsAndLabelsHeights += 6f; } private void DrawTopBar() { EditorGUILayout.LabelField(s_AddControlSchemeLabel, Styles.headerLabel); if (Event.current.type == EventType.Repaint) m_ButtonsAndLabelsHeights += GUILayoutUtility.GetLastRect().height; } private void DrawNameEditTextField() { EditorGUILayout.BeginHorizontal(); var labelSize = EditorStyles.label.CalcSize(s_ControlSchemeNameLabel); EditorGUILayout.LabelField(s_ControlSchemeNameLabel, GUILayout.Width(labelSize.x)); GUI.SetNextControlName("ControlSchemeName"); ////FIXME: This should be a DelayedTextField but for some reason (maybe because it's in a popup?), this //// will lead to the text field not working correctly. Hitting enter on the keyboard will apply the //// change as expected but losing focus will *NOT*. In most cases, this makes the text field seem not //// to work at all so instead we use a normal text field here and then apply the name change as part //// of apply the control scheme changes as a whole. The only real downside is that if the name gets //// adjusted automatically because of a naming conflict, this will only become evident *after* hitting //// the "Save" button. m_ControlScheme.m_Name = EditorGUILayout.TextField(m_ControlScheme.m_Name); if (m_SetFocus) { EditorGUI.FocusTextInControl("ControlSchemeName"); m_SetFocus = false; } EditorGUILayout.EndHorizontal(); } private float DrawRequirementsCheckboxes() { EditorGUILayout.BeginVertical(); EditorGUILayout.LabelField(s_RequirementsLabel, GUILayout.Width(200)); var requirementHeights = GUILayoutUtility.GetLastRect().y; EditorGUI.BeginDisabledGroup(m_DeviceView.index == -1); var requirementsOption = -1; if (m_DeviceView.index >= 0) { var deviceEntryForList = (DeviceEntry)m_DeviceView.list[m_DeviceView.index]; requirementsOption = deviceEntryForList.deviceRequirement.isOptional ? 0 : 1; } EditorGUI.BeginChangeCheck(); requirementsOption = GUILayout.SelectionGrid(requirementsOption, s_RequiredOptionalChoices, 1, EditorStyles.radioButton); requirementHeights += GUILayoutUtility.GetLastRect().y; if (EditorGUI.EndChangeCheck()) m_DeviceList[m_DeviceView.index].deviceRequirement.isOptional = requirementsOption == 0; EditorGUI.EndDisabledGroup(); EditorGUILayout.EndVertical(); return requirementHeights; } private void AddDeviceRequirement(InputControlScheme.DeviceRequirement requirement) { ArrayHelpers.Append(ref m_ControlScheme.m_DeviceRequirements, requirement); m_DeviceList.Add(new DeviceEntry(requirement)); m_DeviceView.index = m_DeviceView.list.Count - 1; editorWindow.Repaint(); } /// /// The control scheme edited by the popup. /// public InputControlScheme controlScheme => m_ControlScheme; private int m_ControlSchemeIndex; private InputControlScheme m_ControlScheme; private Action m_OnApply; private ReorderableList m_DeviceView; private List m_DeviceList = new List(); private int m_RequirementsOptionsChoice; private bool m_SetFocus; private float m_ButtonsAndLabelsHeights; private static Vector2 s_DefaultSize => new Vector2(300, 200); private static readonly GUIContent s_RequirementsLabel = EditorGUIUtility.TrTextContent("Requirements:"); private static readonly GUIContent s_AddControlSchemeLabel = EditorGUIUtility.TrTextContent("Add control scheme"); private static readonly GUIContent s_ControlSchemeNameLabel = EditorGUIUtility.TrTextContent("Scheme Name"); private static readonly string[] s_RequiredOptionalChoices = { "Optional", "Required" }; private static class Styles { public static readonly GUIStyle headerLabel = new GUIStyle(EditorStyles.toolbar) .WithAlignment(TextAnchor.MiddleCenter) .WithFontStyle(FontStyle.Bold) .WithPadding(new RectOffset(10, 6, 0, 0)); } private class DeviceEntry { public string displayText; public InputControlScheme.DeviceRequirement deviceRequirement; public DeviceEntry(InputControlScheme.DeviceRequirement requirement) { displayText = DeviceRequirementToDisplayString(requirement); deviceRequirement = requirement; } public override string ToString() { return displayText; } } } } } #endif // UNITY_EDITOR