Search Unity

Event Like Changeevent That Fires When User Commits Change

Discussion in 'UI Toolkit' started by johnseghersmsft, Apr 10, 2019.

  1. johnseghersmsft

    johnseghersmsft

    Joined:
    May 18, 2015
    Posts:
    28
    I have a custom type (Serializable, but not a UnityEngine.Object) that is a string that is sort of an expandable enum. The possible values default to data in a ScriptableObject, and new values are added to run/edit-time in-memory dictionary any time a new value is entered. There is an editor that allows updating the ScriptableObject data from the in-memory version.

    I implemented a PropertyDrawer using UIElements that (when used with a custom Editor) displays the text field (using PropertyField) and adds a ContextualMenuManipulator to replace the context menu for the field with a list of current options for that value.

    The hard part was dealing with automatically adding a newly-entered value to the list of possible values. I didn't want every edit added--so I needed to wait until the user finished. I tried to add a listener for BlurEvent to the PropertyField, but BlurEvent doesn't bubble up, so it had to be registered to the TextInput field--which I could not find using Querys from PropertyField--possibly because the underlying field was not set up yet during my CreatePropertyGUI() processing???

    So I attach my OnBlur to the TextInput when I get ChangeEvent<string> from the user making changes.

    OnBlur is a necessary, but not sufficient, event. It does not fire when the user clicks somewhere outside the inspector. Furthermore, the PropertyDrawer does not have any form of lifecycle callbacks.

    Instead, I needed register a listener for DetachFromPanelEvent on the PropertyField. This allowed me to know when the panel is being torn down. But at this point I don't have any information about what is being edited.

    I added a local UserData class to hold state information (wouldn't want to register blur callbacks on Every ChangeEvent, after all) and added that to my root VisualElement I created in the CreatePropertyGUI() method. Then, when processing any event, I can walk the parent chain of VisualElements from the target until I find userData of the correct type.

    So--lots of effort to detect all the ways a user can finish editing a value. It would be so much easier if this were exposed as an event fired directly by PropertyField.
     
  2. etienne_phil_unity

    etienne_phil_unity

    Unity Technologies

    Joined:
    Jan 15, 2019
    Posts:
    16
    Hello! Would you have some sample code we could take a look at?
     
  3. johnseghersmsft

    johnseghersmsft

    Joined:
    May 18, 2015
    Posts:
    28
    I've stripped out stuff not germain to the issue, replacing that code with comments like "do something" or "get list of values". The Tag class has a bit more to it, but the one string value is all that's needed for the drawer example here.

    Code (CSharp):
    1.  
    2.     [Serializable]
    3.     public class Tag
    4.     {
    5.         public string TagName;
    6.     }
    7.  
    8.     [CustomPropertyDrawer(typeof(Tag), true)]
    9.     public class TagPropertyDrawer : PropertyDrawer
    10.     {
    11.         private class UserData
    12.         {
    13.             public Tag Tag;
    14.             public bool BlurRegistered;
    15.             public string OriginalValue;
    16.         }
    17.  
    18.         public override VisualElement CreatePropertyGUI(SerializedProperty property)
    19.         {
    20.             var target = GetTagFromSerializedProperty(property);
    21.  
    22.             var container = new VisualElement();
    23.             var userData = new UserData();
    24.             container.userData = userData;
    25.  
    26.             // Create property fields.
    27.             var tagNameField = new PropertyField(
    28.                                     property.FindPropertyRelative("TagName"),
    29.                                     property.displayName);
    30.             ContextualMenuManipulator m = new ContextualMenuManipulator(
    31.                                                 (e) => BuildMenu(e, property));
    32.             m.target = tagNameField;
    33.  
    34.             // This one doesn't work since BlurEvent doesn't bubble up
    35.             tagNameField.RegisterCallback<BlurEvent>(OnBlur);
    36.  
    37.             // ChangeEvent allows me to register OnBlur when user begins
    38.             // making changes
    39.             tagNameField.RegisterCallback<ChangeEvent<string>>(OnChange);
    40.             // OnDetach allows catching cases where BlurEvent does not fire.
    41.             tagNameField.RegisterCallback<DetachFromPanelEvent>(OnDetach);
    42.  
    43.             // keep track of context since PropertyDrawers instances are reused
    44.             userData.BlurRegistered = false;
    45.             userData.Tag = target;
    46.             userData.OriginalValue = target.TagName;
    47.  
    48.             // Add fields to the container.
    49.             container.Add(tagNameField);
    50.  
    51.             return container;
    52.         }
    53.  
    54.         /// <summary>
    55.         /// Walk the parent chain to find one with the correct type of user data.
    56.         /// </summary>
    57.         /// <param name="v"></param>
    58.         /// <returns></returns>
    59.         private UserData GetUserData(VisualElement v)
    60.         {
    61.             var cur = v;
    62.             while (cur != null)
    63.             {
    64.                 if (cur.userData is UserData)
    65.                     return (UserData)cur.userData;
    66.                 cur = cur.parent;
    67.             }
    68.             return null;
    69.         }
    70.  
    71.         // When ChangeEvent occurs, register a callback
    72.         // for BlurEvent on the input field
    73.         private void OnChange(ChangeEvent<string> evt)
    74.         {
    75.             var ve = (VisualElement)evt.target;
    76.             var userData = GetUserData(ve);
    77.  
    78.             if (userData.BlurRegistered)
    79.                 return;
    80.  
    81.             ve.RegisterCallback<BlurEvent>(OnBlur);
    82.             userData.BlurRegistered = true;
    83.         }
    84.  
    85.         // This handles the case where the user clicks outside
    86.         // of the inspector and OnBlur doesn't fire
    87.         private void OnDetach(DetachFromPanelEvent evt)
    88.         {
    89.             var ve = (VisualElement)evt.target;
    90.             var userData = GetUserData(ve);
    91.             ProcessChangedValue(userData);
    92.         }
    93.  
    94.         // This handles the cases where focus changes
    95.         // within the Inspector
    96.         private void OnBlur(BlurEvent evt)
    97.         {
    98.             var ve = (VisualElement)evt.target;
    99.             var userData = GetUserData(ve);
    100.  
    101.             ve.UnregisterCallback<BlurEvent>(OnBlur);
    102.             userData.BlurRegistered = false;
    103.  
    104.             ProcessChangedValue(userData);
    105.         }
    106.  
    107.         // Handle the changed value, called from OnBlur and OnDetach
    108.         private void ProcessChangedValue(UserData userData)
    109.         {
    110.             if (string.IsNullOrEmpty(userData?.Tag?.TagName))
    111.                 return;
    112.  
    113.             if (userData.Tag.TagName != userData.OriginalValue)
    114.             {
    115.                 // Do stuff with new value
    116.             }
    117.         }
    118.  
    119.         // Build popup menu list of tag values
    120.         private void BuildMenu(ContextualMenuPopulateEvent e, SerializedProperty property)
    121.         {
    122.             var tags = // Get list of possible tags
    123.  
    124.             e.menu.AppendSeparator();
    125.             foreach (var tag in tags)
    126.             {
    127.                 e.menu.AppendAction(tag, (menuAction) => target.TagName = (string)menuAction.userData,
    128.                     (menuAction) => DropdownMenuAction.Status.Normal, tag);
    129.             }
    130.             e.StopPropagation();
    131.         }
    132.  
    133.         // Get the non-Object reference from the SerializedProperty
    134.         private Tag GetTagFromSerializedProperty(SerializedProperty property)
    135.         {
    136.             return (Tag)fieldInfo.GetValue(property.serializedObject.targetObject);
    137.         }
    138.  
    139.         private string SafeName(IEventHandler evtHandler)
    140.         {
    141.             return (evtHandler as VisualElement)?.name ?? "NOT A VisualElement";
    142.         }
    143.  
    144.     }
    145. }
    146.  
     
  4. etienne_phil_unity

    etienne_phil_unity

    Unity Technologies

    Joined:
    Jan 15, 2019
    Posts:
    16
    So you have a Tag property whose values is a string picked among a set of available strings, and the user may add new values to that set. I may be missing something but why not have separate widget for tag selection and new tag registration? It seems to me that one could easily add tags by mistake and that most of the time you're likely to want to use an existing tag. Also, there are limitations to the event system, however besides those limitation I guess it's a bit tricky to decide wether a tag should be added or not: basically when the user "leaves" the textfield, it is assumed that if the value held by the textfield is not in the set of available values then it should be added? Finally, I assume that if you let the user create tag you'll end up providing a UI to also delete tags from the set?
     
  5. johnseghersmsft

    johnseghersmsft

    Joined:
    May 18, 2015
    Posts:
    28
    Please focus on the functionality at a field level that is equivalent to the OnValidate method of a class.

    I can debate the usefulness of this particular example, but that is not the point of this request.

    And yes, in the current case, there is a separate editor window where one can manage the values.
     
  6. etienne_phil_unity

    etienne_phil_unity

    Unity Technologies

    Joined:
    Jan 15, 2019
    Posts:
    16
    What is missing in UIElements at the moment would be a way to set wether the ChangeEvent fires on any edit or only on completion, this is on our backlog. In the meantime, here's something you can try, it's obviously a patch but may help a bit: try retrieving the TextField within the PropertyField to set its isDelayed property. You need to wait till the bindings have been set up to do this, here's a quick test that works on my end:
    Code (CSharp):
    1. myPropertyField.RegisterCallback<AttachToPanelEvent>((e) =>
    2. {
    3.      myPropertyField.Q<TextField>().isDelayed = true;
    4. });
     
    AdamBebko and fherbst like this.
  7. johnseghersmsft

    johnseghersmsft

    Joined:
    May 18, 2015
    Posts:
    28
    Thank you! That should provide a better solution than what I have now. It also explains why my Query for the TextField didn't work when I initially tried to attach the BlurEvent to it.

    ChangeEvent vs ChangeCompletedEvent might be a solution worth investigating.
     
    etienne_phil_unity likes this.
  8. D-DutchDave

    D-DutchDave

    Joined:
    May 4, 2018
    Posts:
    36
    Is this a thing yet?
     
    UNrealrealITY likes this.