Search Unity

Reliably detecting changes in control with mixed values

Discussion in 'Immediate Mode GUI (IMGUI)' started by SisusCo, Oct 28, 2019.

  1. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,331
    Hello everybody! I ran into a little conundrum and was wondering if anybody knows of a good solution to it.

    When you want to draw the GUI for some value in Unity, the way it usually works is you call a method with the value passed as a parameter, and then the method returns a value. This is how IntField, Toggle, ObjectField etc. all work. To detect if the value was changed by the user, you simply compare the old value to the returned value. Nice and easy!

    The problem arises when you want to draw a GUI that represents multiple different values within one control.

    You can draw a control that represents multiple values in Unity by setting EditorGUI.showMixedValue to true before calling the method that draws the control. However, when you do this, it only changes the control to visually look like it contains multiple values, but you still can only pass a single value to the method that draws the control and get back only one value as well.

    So the question is: how can you reliably detect changes made by the user through this multi-value control?

    My first thought was to just get the first out of the multiple values that the control represents, and pass that as the parameter to the control drawing method, then compare the return value to that one to detect changes. This worked perfectly well most of the time, however if the user happens to enter the same value as the first value, then the change won't be detected.

    Here is an example that demonstrates the problem:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using System.Linq;
    4.  
    5. public class MixedContentValueChangeDetectionTestWindow : EditorWindow
    6. {
    7.     int[] values = { 1, 2, 3 };
    8.  
    9.     [MenuItem("Window/Mixed Content Value Change Detection Test")]
    10.     private static void Open()
    11.     {
    12.         GetWindow<MixedContentValueChangeDetectionTestWindow>();
    13.     }
    14.  
    15.     private void OnGUI()
    16.     {
    17.         bool mixedValues = values.Distinct().Count() > 1;
    18.         EditorGUI.showMixedValue = mixedValues;
    19.  
    20.         int firstValue = values[0];
    21.  
    22.         int setValues = EditorGUILayout.IntField(firstValue);
    23.  
    24.         if(setValues != firstValue)
    25.         {
    26.             for(int n = values.Length - 1; n >= 0; n--)
    27.             {
    28.                 values[n] = setValues;
    29.             }
    30.         }
    31.         else if(mixedValues)
    32.         {
    33.             // how to detect if value was set to 1 with mixedValues true ?
    34.         }
    35.  
    36.         EditorGUI.showMixedValue = false;
    37.  
    38.         if(GUILayout.Button("Reset"))
    39.         {
    40.             values = new int[]{ 1, 2, 3 };
    41.         }
    42.     }
    43. }
    If you use EditorGUILayout.PropertyField this problem is somehow just handled for you behind the scenes (I think), but if you don't have access to a SerializedProperty, that is not a viable solution.

    What is the best way to handle this scenario? I can think of a couple of different ways to make the detection more accurate, but they all feel a bit hack-y and/or fragile to me.
     
  2. PsyKaw

    PsyKaw

    Joined:
    Aug 16, 2012
    Posts:
    102
    Ghosthowl likes this.
  3. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,331
    Thanks for the answer @PsyKaw !

    The problem with that solution is that EndChangeCheck can return true for all kinds of events besides the value being changed, like when the user first clicks on the text field, presses escape to cancel their input etc.

    I did manage to hack together what seemed to be a reliable way to differentiate between text input events and other events, by testing if the current event type was used and its character wasn't null. However this only works in the specific context of text fields, and thus different solutions would have to be found for all other use cases. Also the tactic won't work, at least without large modifications, with any delayed text fields.

    One "good enough solution" I've found so far, which works well in practice in most cases, is to pass a value to the draw method that is very unlikely to be typed in by the user. So instead of passing the first value (1), pass in something like 1961271802.

    This tactic doesn't work for things like ObjectField though, so different solutions still need to be invented for various other scenarios :confused: