/// <summary> /// Internal implementation of EnumMaskPopup. /// </summary> /// <param name="values"></param> /// <param name="names"></param> /// <param name="position"></param> /// <param name="property"></param> /// <param name="entries">An optional bitmask with selectable entries.</param> /// <param name="isSkippingZero">If true, enum entries with a value of zero are not displayed (useful if the entry already maps to 'None').</param> /// <param name="isDisplayingEverything">If true, if all possible elements are selected (all elements that entries allows), 'Everything' is displayed instead of comma-separated values.</param> /// <param name="isSkippingNonFlags">If true, elements which don't map to a flag value aren't included.</param> private static void EnumMaskPopup(List <int> values, List <string> names, Rect position, SerializedProperty property, int entries = ~0, bool isSkippingZero = false, bool isDisplayingEverything = true, bool isSkippingNonFlags = true) { List <int> valuesCopy; List <string> namesCopy; int omitted = 0; // Popup only supports 32 entries. If exceeded, only use the first 32 and flag the field as red with an error tooltip if (values.Count <= 32) { valuesCopy = new List <int>(values); namesCopy = new List <string>(names); } else { valuesCopy = values.GetRange(0, 32); namesCopy = names.GetRange(0, 32); omitted = values.Count - valuesCopy.Count; } // Get displayable values list, so that we don't have to work with negative values List <int> entriesValues = valuesCopy.Where(en => entries.Contains(en)).ToList(); if (isSkippingZero) { entriesValues.RemoveAll(val => val == 0); } if (isSkippingNonFlags) { entriesValues.RemoveAll(val => !BitmaskUtility.isPowerOfTwo(val)); } List <string> displayNames = namesCopy.Where((en, index) => entriesValues.Contains(valuesCopy[index])).ToList(); // Key: entry value; Value: entriesValues index Dictionary <int, int> existing = new Dictionary <int, int>(); int match; string enumName; // Combine any elements that have a duplicate value for (int i = 0; i < entriesValues.Count; i++) { if (existing.TryGetValue(entriesValues[i], out match)) { enumName = displayNames[i]; displayNames.RemoveAt(i); entriesValues.RemoveAt(i--); // Append the description. (thin space + full-width ampersand + enumName) displayNames[match] += FormatUtility.MARK_AMPERSAND_SAFE + enumName; continue; } else { // Check if we need to use a custom display name for combined-entry value, using format "entry (entry 1 & entry 2 ...)" if (!isSkippingNonFlags && !BitmaskUtility.isPowerOfTwo(entriesValues[i])) { /// First check to make sure all its values are allowed (in entries). Otherwise, remove it List <int> bitmaskValues = BitmaskUtility.GetBitmaskValues(entriesValues[i], valuesCopy.Count); displayNames[i] += " (" + String.Join( FormatUtility.MARK_AMPERSAND_SAFE, // Get all entries in the combined-entry. Then get their display name and append bitmaskValues.Select <int, string>(val => displayNames[entriesValues.IndexOf(val)]).ToArray()) + ")"; } existing.Add(entriesValues[i], i); } } int mask = 0; string displayName = String.Empty; if (property.hasMultipleDifferentValues) { displayName = "—"; } else { // Get selected list, removing any selected that aren't currently allowed List <int> selectedValues = entriesValues.Where(en => property.intValue.Contains(en)).ToList(); // We need to convert the entriesList (which contains potentially skipped values) into an ordered mask (with no skipped values) so that EditorGUI.MaskField can use it // The reason that we iterate over entriesValues instead of selectedValues is so that we can add in duplicated entries (an enum with two entries with the same value) for (int i = 0; i < entriesValues.Count; i++) { if (selectedValues.Contains(entriesValues[i])) { mask |= 1 << i; // Don't display combined-entry values in the label (it clutters things up) if (!isSkippingNonFlags && !BitmaskUtility.isPowerOfTwo(entriesValues[i])) { continue; } // Add a name to display if (String.IsNullOrEmpty(displayName)) { displayName = displayNames[i]; } else { displayName += ", " + displayNames[i]; } } } if (String.IsNullOrEmpty(displayName)) { displayName = "Nothing"; } else if (selectedValues.Count == entriesValues.Count && isDisplayingEverything) { displayName = "Everything"; } } // Make sure the displayName fits in the content field. If not, reduce it. (Subtracts pixels so that it doesn't overlap the dropdown arrow on the right) displayName = FitContent(displayName, position.width - 15, EditorStyles.popup, CutoffOption.ToEntry); int valueNew = 0; // long is used for valueDiff (instead of int) because int will cause an OverflowException when attempting absolute value check for 1 << 31 (Math.Abs(int32.MinValue)). long valueDiff = 0; bool isAdded; // Verify that no values outside of entries are assigned (on any of the selections) foreach (SerializedProperty prop in property.Multiple()) { foreach (int val in BitmaskUtility.GetBitmaskValues(property.intValue)) { if (!entries.Contains(val)) { prop.intValue &= ~val; } } } // Append dashes to the display names for any skipped elements (due to exceeding popup display count of 32) displayNames.AddRange(Enumerable.Repeat <string>(null, omitted)); // Draw popup (without selection display) MultiField( property, (propIter) => valueNew = EditorGUI.MaskField(position, mask, displayNames.ToArray(), PopupWithoutLabelStyle), (propIter) => { // 'Nothing' selected if (valueNew == 0) { property.intValue = 0; return; } // 'Everything' selected if (valueNew == -1) { property.intValue = entries; return; } // Determine what was clicked by getting the difference (bitwise difference) //valueDiff = valueNew ^ mask; valueDiff = (long)valueNew - (long)mask; if (valueDiff == 0) { return; } // Determine if value was added or removed. // (1 << 31 is a negative int value, so its isAdded will need to be flipped. Check against the min value and its two's-complement.) isAdded = valueDiff > 0; if (valueDiff == Int32.MinValue || valueDiff == ~((long)Int32.MinValue - 1)) { isAdded = !isAdded; } // Convert ordered mask to real mask valueDiff = entriesValues[BitmaskUtility.GetMaskIndex(Math.Abs(valueDiff))]; // Standard, add or remove selection if (BitmaskUtility.isPowerOfTwo(valueDiff)) { if (isAdded) { property.intValue |= (int)valueDiff; } else { property.intValue &= (int)~valueDiff; } } // If not a power of two, selection is a combined-entry. Add or remove all its parts else { if (isAdded) { foreach (int val in BitmaskUtility.GetBitmaskValues(valueDiff, valuesCopy.Count)) { property.intValue |= val; } } else { foreach (int val in BitmaskUtility.GetBitmaskValues(valueDiff, valuesCopy.Count)) { property.intValue &= ~val; } } } }); GUIContent label = new GUIContent(displayName); Color colorPrior = GUI.color; if (omitted > 0) { label.tooltip = omitted + " elements were omitted, due to the length of the enum's entries (" + values.Count + ") exceeding Unity's limitation of 32 for popups."; GUI.color = Color.red; } // Display label with active selection EditorGUI.LabelField(position, label, EditorStyles.popup); if (omitted > 0) { GUI.color = colorPrior; } }
/// <summary> /// Gets the index that corresponds to a non-combined bitmask value. /// </summary> /// <exception cref="ArgumentException">Passed enumValue cannot be a combined enum.</exception> /// <param name="enumValue">A non-combined enum mask value.</param> /// <returns></returns> public static int GetMaskIndex(Enum enumValue) { return(BitmaskUtility.GetMaskIndex((int)(object)enumValue)); }