Files
Test/Library/PackageCache/com.unity.collab-proxy@4628d2ba6ea4/Editor/UI/UIElements/MultiSelectDropdown.cs
T
2026-11-01 23:39:55 +02:00

498 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using PlasticGui;
namespace Unity.PlasticSCM.Editor.UI.UIElements
{
internal class MultiSelectDropdownItem
{
internal readonly string Name;
internal readonly Texture Icon;
internal MultiSelectDropdownItem(string name, Texture icon)
{
Name = name;
Icon = icon;
}
}
internal class MultiSelectDropdown : VisualElement
{
internal event Action SelectionChanged;
internal MultiSelectDropdown(string labelPrefix)
{
mLabelPrefix = labelPrefix;
mLabelElement = new TextElement();
mLabelElement.style.unityTextAlign = TextAnchor.MiddleLeft;
mLabelElement.style.flexGrow = 1;
VisualElement chevron = new VisualElement();
chevron.AddToClassList("unity-base-popup-field__arrow");
chevron.style.marginLeft = 4;
mButton = new ToolbarButton(OnButtonClick);
mButton.style.flexDirection = FlexDirection.Row;
mButton.style.unityTextAlign = TextAnchor.MiddleLeft;
mButton.style.height = 20;
mButton.style.paddingBottom = 1;
mButton.focusable = false;
mButton.Add(mLabelElement);
mButton.Add(chevron);
Add(mButton);
UpdateLabel();
UpdateEnabledState();
}
internal void SetItems(IList<MultiSelectDropdownItem> items)
{
mItems = items ?? new List<MultiSelectDropdownItem>();
UpdateLabel();
UpdateEnabledState();
}
internal HashSet<string> Selected
{
get { return mSelected; }
set
{
mSelected = value;
UpdateLabel();
}
}
void OnButtonClick()
{
if (mItems.Count == 0)
return;
MultiSelectDropdownPopupContent content = new MultiSelectDropdownPopupContent(
mItems, mSelected, OnPopupSelectionChanged);
UnityEditor.PopupWindow.Show(mButton.worldBound, content);
}
void OnPopupSelectionChanged(HashSet<string> newSelected)
{
mSelected = newSelected;
UpdateLabel();
if (SelectionChanged != null)
SelectionChanged();
}
void UpdateLabel()
{
mLabelElement.text = BuildLabel();
}
string BuildLabel()
{
if (mSelected == null)
return mLabelPrefix;
string firstSelected;
int totalSelected;
CountVisibleSelected(out firstSelected, out totalSelected);
if (totalSelected == 0)
return string.Format("{0}: {1}",
mLabelPrefix,
PlasticLocalization.Name.MultiSelectNoneSelected.GetString());
if (totalSelected == 1)
return string.Format("{0}: {1}", mLabelPrefix, firstSelected);
return string.Format("{0}: {1}",
mLabelPrefix,
PlasticLocalization.Name.MultiSelectOverflowFormat.GetString(
firstSelected, totalSelected - 1));
}
void CountVisibleSelected(out string firstSelected, out int totalSelected)
{
firstSelected = null;
totalSelected = 0;
foreach (MultiSelectDropdownItem item in mItems)
{
if (!mSelected.Contains(item.Name))
continue;
if (firstSelected == null)
firstSelected = item.Name;
totalSelected++;
}
}
void UpdateEnabledState()
{
mButton.SetEnabled(mItems.Count > 0);
}
readonly string mLabelPrefix;
readonly TextElement mLabelElement;
readonly ToolbarButton mButton;
IList<MultiSelectDropdownItem> mItems = new List<MultiSelectDropdownItem>();
HashSet<string> mSelected;
}
internal class MultiSelectDropdownPopupContent : PopupWindowContent
{
internal MultiSelectDropdownPopupContent(
IList<MultiSelectDropdownItem> items,
HashSet<string> currentSelection,
Action<HashSet<string>> onSelectionChanged)
{
mAllItems = items;
mFilteredItems = new List<MultiSelectDropdownItem>(items);
mSelected = currentSelection == null
? null
: new HashSet<string>(currentSelection);
mOnSelectionChanged = onSelectionChanged;
}
public override Vector2 GetWindowSize()
{
int itemCount = mAllItems == null ? 0 : mAllItems.Count;
float contentHeight = SEARCH_AREA_HEIGHT
+ (itemCount * ROW_HEIGHT)
+ FOOTER_HEIGHT;
float height = Mathf.Clamp(contentHeight, POPUP_HEIGHT_MIN, POPUP_HEIGHT_MAX);
return new Vector2(POPUP_WIDTH, height);
}
public override void OnOpen()
{
BuildGUI(editorWindow.rootVisualElement);
}
public override void OnGUI(Rect rect)
{
if (Event.current.type != EventType.KeyDown)
return;
if (Event.current.keyCode != KeyCode.Escape)
return;
editorWindow.Close();
Event.current.Use();
}
void BuildGUI(VisualElement root)
{
root.style.flexDirection = FlexDirection.Column;
ToolbarSearchField searchField = new ToolbarSearchField();
searchField.style.width = POPUP_WIDTH - 2 * SEARCH_HORIZONTAL_MARGIN;
searchField.style.marginTop = SEARCH_HORIZONTAL_MARGIN;
searchField.style.marginLeft = SEARCH_HORIZONTAL_MARGIN;
searchField.style.marginRight = SEARCH_HORIZONTAL_MARGIN;
searchField.style.marginBottom = SEARCH_HORIZONTAL_MARGIN;
searchField.RegisterValueChangedCallback(OnSearchChanged);
root.Add(searchField);
mListView = new ListView();
mListView.itemsSource = mFilteredItems;
mListView.fixedItemHeight = ROW_HEIGHT;
mListView.makeItem = MakeRow;
mListView.bindItem = BindRow;
mListView.selectionType = SelectionType.None;
mListView.style.flexGrow = 1;
mListView.style.minHeight = 0;
root.Add(mListView);
VisualElement footer = new VisualElement();
footer.style.flexDirection = FlexDirection.Row;
footer.style.justifyContent = Justify.FlexEnd;
footer.style.flexShrink = 0;
footer.style.paddingLeft = 4;
footer.style.paddingRight = 4;
footer.style.paddingBottom = 4;
footer.style.paddingTop = 4;
footer.style.borderTopWidth = 1;
footer.style.borderTopColor = UnityStyles.Colors.BarBorder;
Button resetButton = new Button(OnResetClick);
resetButton.text = PlasticLocalization.Name.MultiSelectResetButton.GetString();
resetButton.style.fontSize = ROW_FONT_SIZE;
resetButton.style.minWidth = 60;
footer.Add(resetButton);
root.Add(footer);
}
VisualElement MakeRow()
{
VisualElement row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.style.paddingLeft = ROW_HORIZONTAL_PADDING;
row.style.paddingRight = ROW_HORIZONTAL_PADDING;
row.style.height = ROW_HEIGHT;
Image icon = new Image();
icon.name = ROW_ICON_NAME;
icon.style.width = ICON_SIZE;
icon.style.height = ICON_SIZE;
icon.style.marginRight = 4;
icon.scaleMode = ScaleMode.ScaleToFit;
row.Add(icon);
Label label = new Label();
label.name = ROW_LABEL_NAME;
label.style.flexGrow = 1;
label.style.fontSize = ROW_FONT_SIZE;
label.style.unityTextAlign = TextAnchor.MiddleLeft;
label.style.overflow = Overflow.Hidden;
label.style.textOverflow = TextOverflow.Ellipsis;
label.style.marginLeft = 0;
label.style.marginRight = 0;
label.style.paddingLeft = 0;
label.style.paddingRight = 0;
row.Add(label);
Button hoverLink = new Button();
hoverLink.name = ROW_HOVER_LINK_NAME;
hoverLink.style.color = UnityStyles.Colors.Link;
hoverLink.style.fontSize = ROW_FONT_SIZE;
hoverLink.style.marginLeft = 6;
hoverLink.style.marginRight = 6;
hoverLink.style.paddingLeft = 0;
hoverLink.style.paddingRight = 0;
hoverLink.style.borderTopWidth = 0;
hoverLink.style.borderBottomWidth = 0;
hoverLink.style.borderLeftWidth = 0;
hoverLink.style.borderRightWidth = 0;
hoverLink.style.backgroundColor = Color.clear;
hoverLink.style.display = DisplayStyle.None;
hoverLink.SetMouseCursor(MouseCursor.Link);
hoverLink.clicked += () => OnHoverLinkClick(hoverLink);
row.Add(hoverLink);
Toggle checkbox = new Toggle();
checkbox.name = ROW_CHECKBOX_NAME;
checkbox.style.marginLeft = 0;
checkbox.style.marginRight = 0;
checkbox.RegisterValueChangedCallback(OnCheckboxChanged);
row.Add(checkbox);
row.RegisterCallback<MouseEnterEvent>(OnRowMouseEnter);
row.RegisterCallback<MouseLeaveEvent>(OnRowMouseLeave);
return row;
}
void BindRow(VisualElement element, int index)
{
if (index < 0 || index >= mFilteredItems.Count)
return;
MultiSelectDropdownItem item = mFilteredItems[index];
Image icon = element.Q<Image>(ROW_ICON_NAME);
Label label = element.Q<Label>(ROW_LABEL_NAME);
Toggle checkbox = element.Q<Toggle>(ROW_CHECKBOX_NAME);
Button hoverLink = element.Q<Button>(ROW_HOVER_LINK_NAME);
icon.image = item.Icon;
icon.style.display = item.Icon != null ? DisplayStyle.Flex : DisplayStyle.None;
label.text = item.Name;
bool isChecked = IsItemSelected(item.Name);
checkbox.SetValueWithoutNotify(isChecked);
checkbox.userData = item.Name;
hoverLink.userData = item.Name;
UpdateHoverLink(hoverLink, item.Name);
}
void UpdateHoverLink(Button hoverLink, string itemName)
{
if (mHoveredItem != itemName)
{
hoverLink.style.display = DisplayStyle.None;
return;
}
hoverLink.text = IsOnlySelected(itemName)
? PlasticLocalization.Name.MultiSelectSelectAllAction.GetString()
: PlasticLocalization.Name.MultiSelectOnlyAction.GetString();
hoverLink.style.display = DisplayStyle.Flex;
}
bool IsItemSelected(string itemName)
{
return mSelected == null || mSelected.Contains(itemName);
}
void OnRowMouseEnter(MouseEnterEvent evt)
{
VisualElement row = (VisualElement)evt.currentTarget;
Button hoverLink = row.Q<Button>(ROW_HOVER_LINK_NAME);
mHoveredItem = (string)hoverLink.userData;
row.style.backgroundColor = UnityStyles.Colors.HoverBackgroundColor;
UpdateHoverLink(hoverLink, mHoveredItem);
}
void OnRowMouseLeave(MouseLeaveEvent evt)
{
VisualElement row = (VisualElement)evt.currentTarget;
Button hoverLink = row.Q<Button>(ROW_HOVER_LINK_NAME);
string itemName = (string)hoverLink.userData;
if (mHoveredItem == itemName)
mHoveredItem = null;
row.style.backgroundColor = StyleKeyword.Null;
hoverLink.style.display = DisplayStyle.None;
}
void OnHoverLinkClick(Button hoverLink)
{
string itemName = (string)hoverLink.userData;
if (IsOnlySelected(itemName))
{
mSelected = null;
}
else
{
mSelected = new HashSet<string>();
mSelected.Add(itemName);
}
FireSelectionChanged();
mListView.RefreshItems();
}
void OnCheckboxChanged(ChangeEvent<bool> evt)
{
Toggle checkbox = (Toggle)evt.currentTarget;
string itemName = (string)checkbox.userData;
if (mSelected == null)
{
mSelected = new HashSet<string>();
foreach (MultiSelectDropdownItem item in mAllItems)
mSelected.Add(item.Name);
}
if (evt.newValue)
mSelected.Add(itemName);
else
mSelected.Remove(itemName);
if (IsAllSelected())
mSelected = null;
FireSelectionChanged();
}
bool IsAllSelected()
{
if (mSelected == null)
return true;
if (mSelected.Count != mAllItems.Count)
return false;
foreach (MultiSelectDropdownItem item in mAllItems)
{
if (!mSelected.Contains(item.Name))
return false;
}
return true;
}
void OnSearchChanged(ChangeEvent<string> evt)
{
ApplySearchFilter(evt.newValue);
}
void ApplySearchFilter(string query)
{
mFilteredItems.Clear();
if (string.IsNullOrEmpty(query))
{
foreach (MultiSelectDropdownItem item in mAllItems)
mFilteredItems.Add(item);
}
else
{
string lower = query.ToLowerInvariant();
foreach (MultiSelectDropdownItem item in mAllItems)
{
if (item.Name.ToLowerInvariant().Contains(lower))
mFilteredItems.Add(item);
}
}
mListView.itemsSource = mFilteredItems;
mListView.Rebuild();
}
void OnResetClick()
{
mSelected = null;
FireSelectionChanged();
mListView.RefreshItems();
}
bool IsOnlySelected(string itemName)
{
return mSelected != null
&& mSelected.Count == 1
&& mSelected.Contains(itemName);
}
void FireSelectionChanged()
{
if (mOnSelectionChanged == null)
return;
HashSet<string> snapshot = mSelected == null
? null
: new HashSet<string>(mSelected);
mOnSelectionChanged(snapshot);
}
readonly IList<MultiSelectDropdownItem> mAllItems;
readonly List<MultiSelectDropdownItem> mFilteredItems;
readonly Action<HashSet<string>> mOnSelectionChanged;
HashSet<string> mSelected;
string mHoveredItem;
ListView mListView;
const float POPUP_WIDTH = 280f;
const float POPUP_HEIGHT_MIN = 80f;
const float POPUP_HEIGHT_MAX = 360f;
const float SEARCH_AREA_HEIGHT = 28f;
const float FOOTER_HEIGHT = 32f;
const int ROW_HEIGHT = 18;
const int ROW_HORIZONTAL_PADDING = 4;
const int ROW_FONT_SIZE = 12;
const int ICON_SIZE = 14;
const int SEARCH_HORIZONTAL_MARGIN = 4;
const string ROW_ICON_NAME = "icon";
const string ROW_LABEL_NAME = "label";
const string ROW_HOVER_LINK_NAME = "hoverLink";
const string ROW_CHECKBOX_NAME = "checkbox";
}
}