Added Unity project files
This commit is contained in:
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32b17ad88ef74de8865aa6e2193558a9
|
||||
timeCreated: 1736896912
|
||||
@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEngine.UIElements.Experimental;
|
||||
|
||||
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
public class BuilderProgress: VisualElement
|
||||
{
|
||||
public new class UxmlFactory : UxmlFactory<BuilderProgress, UxmlTraits> {}
|
||||
|
||||
public new class UxmlTraits : VisualElement.UxmlTraits
|
||||
{
|
||||
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
|
||||
{
|
||||
get { yield break; }
|
||||
}
|
||||
}
|
||||
|
||||
public struct ProgressBarStateData
|
||||
{
|
||||
public bool Visible { get; set; }
|
||||
public string Text { get; set; }
|
||||
public float Progress { get; set; }
|
||||
}
|
||||
|
||||
public ProgressBarStateData State => _state;
|
||||
public EventHandler OnCancel;
|
||||
|
||||
private ProgressBarStateData _state;
|
||||
private readonly VisualElement _progressBlock;
|
||||
private readonly VisualElement _progressBar;
|
||||
private readonly Label _progressText;
|
||||
private readonly Button _cancelButton;
|
||||
private VisualElement _visualRoot;
|
||||
|
||||
|
||||
public BuilderProgress()
|
||||
{
|
||||
Resources.Load<VisualTreeAsset>("BuilderProgress").CloneTree(this);
|
||||
styleSheets.Add(Resources.Load<StyleSheet>("BuilderProgressStyles"));
|
||||
RegisterCallback<AttachToPanelEvent>(evt =>
|
||||
{
|
||||
_visualRoot = evt.destinationPanel.visualTree;
|
||||
});
|
||||
_progressBlock = this;
|
||||
_progressBlock.AddToClassList("d-none");
|
||||
_progressBar = this.Q("progress-bar");
|
||||
_progressText = this.Q<Label>("progress-text");
|
||||
_cancelButton = this.Q<Button>("cancel-button");
|
||||
_cancelButton.clicked += () => OnCancel?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
|
||||
public void SetProgress(ProgressBarStateData state)
|
||||
{
|
||||
if (_state.Visible != state.Visible)
|
||||
{
|
||||
_progressBlock.EnableInClassList("d-none", !state.Visible);
|
||||
if (state.Visible)
|
||||
{
|
||||
_progressBar.style.width = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_progressText.text = state.Text;
|
||||
if (Mathf.Abs(_state.Progress - state.Progress) > float.Epsilon)
|
||||
{
|
||||
// execute on next frame to allow for layout to calculate
|
||||
_visualRoot.schedule.Execute(() =>
|
||||
{
|
||||
_progressBar.experimental.animation.Start(
|
||||
new StyleValues {width = _progressBar.layout.width, height = 28f},
|
||||
new StyleValues {width = _progressBlock.layout.width * state.Progress, height = 28f},
|
||||
500
|
||||
);
|
||||
}).StartingIn(50);
|
||||
}
|
||||
_state = state;
|
||||
}
|
||||
|
||||
public void ClearProgress()
|
||||
{
|
||||
_progressBar.style.width = 0;
|
||||
}
|
||||
|
||||
public void HideProgress()
|
||||
{
|
||||
SetProgress(new ProgressBarStateData { Visible = false });
|
||||
}
|
||||
|
||||
public void SetCancelButtonVisibility(bool isVisible)
|
||||
{
|
||||
_cancelButton.EnableInClassList("d-none", !isVisible);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20ece8432d114cd18c506eada5258014
|
||||
timeCreated: 1736896921
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b85293d7e174d5f8b6348567c2c5df2
|
||||
timeCreated: 1736897025
|
||||
@ -0,0 +1,9 @@
|
||||
<UXML xmlns="UnityEngine.UIElements">
|
||||
<Label name="progress-text" text="Refreshing data..." class="progress-text text-white mb-2" />
|
||||
<VisualElement name="progress" class="progress-container">
|
||||
<VisualElement name="progress-bar" class="progress-bar" />
|
||||
</VisualElement>
|
||||
<VisualElement class="mt-2">
|
||||
<Button name="cancel-button" class="flex-grow-1 d-none m-0 text-bold" text="Cancel Upload" />
|
||||
</VisualElement>
|
||||
</UXML>
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 64cf856a19d24b1eafc2c14d18093846
|
||||
timeCreated: 1736897068
|
||||
@ -0,0 +1,45 @@
|
||||
BuilderProgress {
|
||||
position: absolute;
|
||||
background-color: rgba(0,0,0,0.9);
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
background-color: #363636;
|
||||
margin: 4px 0;
|
||||
height: 24px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top:0;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background-color: #006FF8;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
-unity-text-align: middle-center;
|
||||
-unity-font-style: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#cancel-button {
|
||||
font-size: 14px;
|
||||
padding: 8px 24px 8px 24px;
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7df871579544480afc16fbd395e75ae
|
||||
timeCreated: 1736897033
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0a430a4db4c4ac0bc2fbd23c0d2b954
|
||||
timeCreated: 1687542367
|
||||
@ -0,0 +1,113 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
public class Checklist: VisualElement
|
||||
{
|
||||
public new class UxmlFactory : UxmlFactory<Checklist, UxmlTraits> {}
|
||||
public new class UxmlTraits : VisualElement.UxmlTraits
|
||||
{
|
||||
private readonly UxmlStringAttributeDescription _label = new UxmlStringAttributeDescription { name = "label" };
|
||||
private readonly UxmlStringAttributeDescription _iconName = new UxmlStringAttributeDescription { name = "icon-name" };
|
||||
|
||||
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
|
||||
{
|
||||
get { yield break; }
|
||||
}
|
||||
|
||||
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
|
||||
{
|
||||
base.Init(ve, bag, cc);
|
||||
var checklistField = (Checklist) ve;
|
||||
var label = _label.GetValueFromBag(bag, cc);
|
||||
if (string.IsNullOrWhiteSpace(label))
|
||||
{
|
||||
checklistField.Q("checklist-label").AddToClassList("d-none");
|
||||
}
|
||||
else
|
||||
{
|
||||
checklistField.Q<Label>("checklist-label-text").text = label;
|
||||
}
|
||||
|
||||
var iconName = _iconName.GetValueFromBag(bag, cc);
|
||||
if (string.IsNullOrWhiteSpace(iconName))
|
||||
{
|
||||
checklistField.Q("checklist-label-icon").AddToClassList("d-none");
|
||||
}
|
||||
else
|
||||
{
|
||||
var icon = EditorGUIUtility.IconContent(iconName);
|
||||
var darkIcon = EditorGUIUtility.IconContent($"d_{iconName}");
|
||||
if (EditorGUIUtility.isProSkin && darkIcon != null)
|
||||
{
|
||||
checklistField.Q("checklist-label-icon").style.backgroundImage = (Texture2D) darkIcon.image;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ChecklistItem
|
||||
{
|
||||
public string Value { get; set; }
|
||||
public string Label { get; set; }
|
||||
public bool Checked { get; set; }
|
||||
}
|
||||
|
||||
private VisualElement _itemsContainer;
|
||||
private List<ChecklistItem> _items;
|
||||
|
||||
public List<ChecklistItem> Items
|
||||
{
|
||||
get => _items;
|
||||
set
|
||||
{
|
||||
_items = value;
|
||||
RenderItems();
|
||||
}
|
||||
}
|
||||
|
||||
public Checklist()
|
||||
{
|
||||
Resources.Load<VisualTreeAsset>("ChecklistLayout").CloneTree(this);
|
||||
styleSheets.Add(Resources.Load<StyleSheet>("ChecklistStyles"));
|
||||
_itemsContainer = this.Q("checklist-items");
|
||||
_items = new List<ChecklistItem>();
|
||||
}
|
||||
|
||||
public void MarkItem(string value, bool checkState)
|
||||
{
|
||||
var itemIndex = _items.FindIndex(i => i.Value == value);
|
||||
if (itemIndex < 0) return;
|
||||
|
||||
_items[itemIndex].Checked = checkState;
|
||||
RenderItems();
|
||||
}
|
||||
|
||||
private void RenderItems()
|
||||
{
|
||||
_itemsContainer.Clear();
|
||||
foreach (var item in _items)
|
||||
{
|
||||
var container = new VisualElement();
|
||||
container.AddToClassList("row");
|
||||
container.AddToClassList("align-items-center");
|
||||
var icon = new VisualElement();
|
||||
icon.AddToClassList("icon");
|
||||
icon.AddToClassList(item.Checked ? "check-icon" : "cross-icon");
|
||||
container.Add(icon);
|
||||
var label = new Label(item.Label)
|
||||
{
|
||||
name = item.Value
|
||||
};
|
||||
container.Add(label);
|
||||
_itemsContainer.Add(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c1877d18642b4765bdeba2e1cb631978
|
||||
timeCreated: 1687542376
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae06c41f028e44fda8feb04e8942b483
|
||||
timeCreated: 1687543126
|
||||
@ -0,0 +1,9 @@
|
||||
<UXML xmlns="UnityEngine.UIElements">
|
||||
<VisualElement class="col" name="checklist-block">
|
||||
<VisualElement name="checklist-label" class="mb-2 row section-header">
|
||||
<VisualElement class="icon" name="checklist-label-icon" />
|
||||
<Label name="checklist-label-text" />
|
||||
</VisualElement>
|
||||
<VisualElement name="checklist-items" class="col w-100 mb-2" />
|
||||
</VisualElement>
|
||||
</UXML>
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa4cfed5918f428db418a0c5ee52de02
|
||||
timeCreated: 1687543136
|
||||
@ -0,0 +1,60 @@
|
||||
#checklist-block {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.dark #checklist-block {
|
||||
border-bottom-color: rgb(26, 26, 26);
|
||||
}
|
||||
|
||||
.light #checklist-block {
|
||||
border-bottom-color: rgb(127, 127, 127);
|
||||
}
|
||||
|
||||
|
||||
/*.light #checklist-block {*/
|
||||
/* border-color: #A9A9A9;*/
|
||||
/* background-color: rgba(235, 235, 235, 0.2039216);*/
|
||||
/* color: #161616;*/
|
||||
/*}*/
|
||||
|
||||
/*.dark #checklist-block {*/
|
||||
/* border-color: #232323;*/
|
||||
/* background-color: rgba(96, 96, 96, 0.2039216);*/
|
||||
/* color: #BDBDBD;*/
|
||||
/*}*/
|
||||
|
||||
#checklist-block #checklist-label {
|
||||
-unity-font-style: bold;
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
#checklist-block #checklist-label #checklist-label-text {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
#checklist-items .icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.cross-icon {
|
||||
background-image: resource("DotFill");
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
background-image: resource("P4_CheckOutRemote");
|
||||
}
|
||||
|
||||
|
||||
.light .create-icon {
|
||||
background-image: resource("CreateAddNew");
|
||||
}
|
||||
|
||||
.dark .create-icon {
|
||||
background-image: resource("d_CreateAddNew");
|
||||
}
|
||||
|
||||
#checklist-items Label {
|
||||
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b3c8c1b61ee451892e761c0d1b0d60e
|
||||
timeCreated: 1687543390
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9949ae8c0304d43a461cbf34f7588e8
|
||||
timeCreated: 1687997045
|
||||
@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
public class ContentWarningsField : OptionsPopupField<string>
|
||||
{
|
||||
private static readonly string[] CONTENT_WARNING_TAGS = { "content_sex", "content_adult", "content_violence", "content_gore", "content_horror" };
|
||||
|
||||
protected override IList<string> GetOptions()
|
||||
{
|
||||
return CONTENT_WARNING_TAGS;
|
||||
}
|
||||
|
||||
protected override string GetOptionName(string option)
|
||||
{
|
||||
return option switch
|
||||
{
|
||||
"content_sex" => "Sexually Suggestive",
|
||||
"content_adult" => "Adult Language and Themes",
|
||||
"content_violence" => "Graphic Violence",
|
||||
"content_gore" => "Excessive Gore",
|
||||
"content_horror" => "Extreme Horror",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool IsOptionLocked(string option)
|
||||
{
|
||||
return OriginalOptions.Contains("admin_content_reviewed") && OriginalOptions.Contains(option);
|
||||
}
|
||||
|
||||
protected override int GetOptionCount()
|
||||
{
|
||||
return CONTENT_WARNING_TAGS.Length;
|
||||
}
|
||||
|
||||
public new class UxmlFactory : UxmlFactory<ContentWarningsField, UxmlTraits> { }
|
||||
public new class UxmlTraits : OptionsPopupField<string>.UxmlTraits { }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9246484d22834713af8e88e23d611272
|
||||
timeCreated: 1687997117
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3fdc0c0682d4191875827b6ee5ebba4
|
||||
timeCreated: 1691092601
|
||||
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace VRC.SDK3.Editor.Elements
|
||||
{
|
||||
public class GenericBuilderNotification: VisualElement
|
||||
{
|
||||
public GenericBuilderNotification(string text, string details = null, string actionText = null, Action action = null)
|
||||
{
|
||||
Resources.Load<VisualTreeAsset>("GenericBuilderNotification").CloneTree(this);
|
||||
styleSheets.Add(Resources.Load<StyleSheet>("GenericBuilderNotificationStyles"));
|
||||
|
||||
this.Q<Label>("main-text").text = text;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(details))
|
||||
{
|
||||
var detailsLabel = this.Q<Label>("details-text");
|
||||
detailsLabel.text = details;
|
||||
detailsLabel.RemoveFromClassList("d-none");
|
||||
}
|
||||
|
||||
if (action != null)
|
||||
{
|
||||
var actionButton = this.Q<Button>("action-button");
|
||||
actionButton.text = actionText;
|
||||
actionButton.clicked += action;
|
||||
actionButton.RemoveFromClassList("d-none");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7485de206c641469dcf232a342a69c6
|
||||
timeCreated: 1691093163
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6cb017af4fd04b19835d3a5069a6880f
|
||||
timeCreated: 1691092607
|
||||
@ -0,0 +1,7 @@
|
||||
<UXML xmlns="UnityEngine.UIElements">
|
||||
<VisualElement name="notification-block" class="col mt-2 mb-2 w-100 flex-shrink-0">
|
||||
<Label class="text-lg" name="main-text" />
|
||||
<Label name="details-text" class="mt-2 p-2 white-space-normal flex-grow-1 d-none" />
|
||||
<Button class="mt-3 pl-4 pr-4 pt-2 pb-2 text-bold d-none" name="action-button" />
|
||||
</VisualElement>
|
||||
</UXML>
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6e653ccec8c748c79b9bcf3b98428441
|
||||
timeCreated: 1691092618
|
||||
@ -0,0 +1,3 @@
|
||||
#details-text {
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 98fa0a3c12e04eeaac24aa52eed81e8a
|
||||
timeCreated: 1691093136
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2507f59cbb8f43d88cfb5fb6974309bb
|
||||
timeCreated: 1732148656
|
||||
@ -0,0 +1,242 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
|
||||
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
public class Modal : VisualElement
|
||||
{
|
||||
public new class UxmlFactory : UxmlFactory<Modal, UxmlTraits> {}
|
||||
public new class UxmlTraits : VisualElement.UxmlTraits
|
||||
{
|
||||
private readonly UxmlStringAttributeDescription _title = new() { name = "title" };
|
||||
|
||||
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new UxmlChildElementDescription(typeof(VisualElement));
|
||||
}
|
||||
}
|
||||
|
||||
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
|
||||
{
|
||||
base.Init(ve, bag, cc);
|
||||
var modal = (Modal) ve;
|
||||
modal._title.text = _title.GetValueFromBag(bag, cc);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Label _title;
|
||||
private readonly Button _closeButton;
|
||||
private readonly VisualElement _container;
|
||||
private readonly VisualElement _contentWrapper;
|
||||
private readonly VisualElement _actionButtonWrapper;
|
||||
private readonly Button _actionButton;
|
||||
private readonly VisualElement _icon;
|
||||
private StyleLength _parentHeight;
|
||||
|
||||
private bool _hasActionButton;
|
||||
|
||||
public override VisualElement contentContainer => _container;
|
||||
|
||||
[PublicAPI]
|
||||
public EventHandler OnClose;
|
||||
[PublicAPI]
|
||||
public bool IsOpen { get; private set; }
|
||||
[PublicAPI]
|
||||
public EventHandler OnCancel;
|
||||
|
||||
private VisualElement _anchor;
|
||||
private VisualElement _originalParent;
|
||||
|
||||
private bool _isTemporary;
|
||||
|
||||
public Modal()
|
||||
{
|
||||
Resources.Load<VisualTreeAsset>("Modal").CloneTree(this);
|
||||
styleSheets.Add(Resources.Load<StyleSheet>("ModalStyles"));
|
||||
|
||||
AddToClassList("d-none");
|
||||
AddToClassList("absolute");
|
||||
AddToClassList("col");
|
||||
|
||||
_title = this.Q<Label>("modal-title");
|
||||
_closeButton = this.Q<Button>("modal-close-btn");
|
||||
_contentWrapper = this.Q("modal-content-wrapper");
|
||||
_container = _contentWrapper.Q("modal-content");
|
||||
_icon = this.Q<VisualElement>("modal-icon");
|
||||
_actionButtonWrapper = this.Q("modal-action-button-wrapper");
|
||||
_actionButton = this.Q<Button>("modal-action-button");
|
||||
var backdrop = this.Q("modal-backdrop");
|
||||
|
||||
|
||||
RegisterCallback<AttachToPanelEvent>(_ =>
|
||||
{
|
||||
// Only save the initial parent, ignoring the future re-parenting
|
||||
if (_originalParent != null) return;
|
||||
_originalParent = parent;
|
||||
});
|
||||
|
||||
void OnCloseCancel()
|
||||
{
|
||||
// If there is an action button, treat the backdrop/close click as a cancel
|
||||
if (_hasActionButton)
|
||||
{
|
||||
OnCancel?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
_closeButton.clicked += OnCloseCancel;
|
||||
backdrop.RegisterCallback<MouseDownEvent>(_ =>
|
||||
{
|
||||
OnCloseCancel();
|
||||
});
|
||||
}
|
||||
|
||||
public Modal(VisualElement anchor) : this()
|
||||
{
|
||||
_anchor = anchor;
|
||||
}
|
||||
|
||||
public Modal(string title, string content, VisualElement anchor) : this(anchor)
|
||||
{
|
||||
_title.text = title;
|
||||
var splitContent = content.Split('\n');
|
||||
_container.AddToClassList("p-3");
|
||||
foreach (var line in splitContent)
|
||||
{
|
||||
var label = new Label(line)
|
||||
{
|
||||
style =
|
||||
{
|
||||
whiteSpace = WhiteSpace.Normal
|
||||
}
|
||||
};
|
||||
_container.Add(label);
|
||||
}
|
||||
}
|
||||
|
||||
public Modal(string title, string content, Action buttonAction, string buttonActionText, VisualElement anchor) : this(title, content, anchor)
|
||||
{
|
||||
_actionButton.clicked += () =>
|
||||
{
|
||||
buttonAction?.Invoke();
|
||||
Close();
|
||||
};
|
||||
_actionButtonWrapper.RemoveFromClassList("d-none");
|
||||
_actionButton.text = !string.IsNullOrWhiteSpace(buttonActionText) ? buttonActionText : "OK";
|
||||
_hasActionButton = true;
|
||||
_container.AddToClassList("mr-2");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shorthand method for creating and showing a modal in place
|
||||
/// Calling `Close` on such a modal - immediately removes it from the hierarchy
|
||||
/// </summary>
|
||||
/// <param name="title"></param>
|
||||
/// <param name="content"></param>
|
||||
/// <param name="anchor"></param>
|
||||
/// <returns></returns>
|
||||
[PublicAPI]
|
||||
public static Modal CreateAndShow(string title, string content, VisualElement anchor)
|
||||
{
|
||||
var modal = new Modal(title, content, anchor)
|
||||
{
|
||||
_isTemporary = true
|
||||
};
|
||||
anchor.Add(modal);
|
||||
modal.Open();
|
||||
return modal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shorthand method for creating and showing a modal in place
|
||||
/// Calling `Close` on such a modal - immediately removes it from the hierarchy
|
||||
/// </summary>
|
||||
/// <param name="title"></param>
|
||||
/// <param name="content"></param>
|
||||
/// <param name="anchor"></param>
|
||||
/// <param name="buttonAction">If provided - adds a button that calls this action</param>
|
||||
/// <param name="buttonActionText">Sets the text of the action button if `buttonAction` is provided</param>
|
||||
/// <returns></returns>
|
||||
[PublicAPI]
|
||||
public static Modal CreateAndShow(string title, string content, Action buttonAction, string buttonActionText, VisualElement anchor)
|
||||
{
|
||||
var modal = new Modal(title, content, buttonAction, buttonActionText, anchor)
|
||||
{
|
||||
_isTemporary = true
|
||||
};
|
||||
anchor.Add(modal);
|
||||
modal.Open();
|
||||
return modal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the element to re-anchor into
|
||||
/// </summary>
|
||||
/// <param name="anchor"></param>
|
||||
[PublicAPI]
|
||||
public void SetAnchor(VisualElement anchor)
|
||||
{
|
||||
_anchor = anchor;
|
||||
RemoveFromHierarchy();
|
||||
_anchor.Add(this);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public void Open()
|
||||
{
|
||||
if (IsOpen) return;
|
||||
IsOpen = true;
|
||||
RemoveFromClassList("d-none");
|
||||
_parentHeight = parent.style.height;
|
||||
schedule.Execute(() =>
|
||||
{
|
||||
if (parent.contentRect.height < layout.height + 40)
|
||||
{
|
||||
parent.style.height = layout.height + 40;
|
||||
}
|
||||
}).ExecuteLater(1);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public void Close()
|
||||
{
|
||||
if (!IsOpen) return;
|
||||
IsOpen = false;
|
||||
AddToClassList("d-none");
|
||||
parent.style.height = _parentHeight;
|
||||
OnClose?.Invoke(this, EventArgs.Empty);
|
||||
// If there is no action button - treat any close as cancel
|
||||
if (!_hasActionButton)
|
||||
{
|
||||
OnCancel?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
if (_isTemporary)
|
||||
{
|
||||
RemoveFromHierarchy();
|
||||
}
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public void SetTitle(string title)
|
||||
{
|
||||
_title.text = title;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public void SetIcon(string resourceName)
|
||||
{
|
||||
_icon.RemoveFromClassList("d-none");
|
||||
_icon.style.backgroundImage = new StyleBackground(Resources.Load<Texture2D>(resourceName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ba9be7e15ca4b2d90dcf11932772a39
|
||||
timeCreated: 1732148710
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f55c7e8e734400eb64c21091a186b9b
|
||||
timeCreated: 1732148665
|
||||
@ -0,0 +1,16 @@
|
||||
<UXML xmlns="UnityEngine.UIElements">
|
||||
<VisualElement name="modal-backdrop" class="absolute" />
|
||||
<VisualElement name="modal-container" class="col">
|
||||
<VisualElement class="row p-2 justify-container-between align-items-center" name="modal-header">
|
||||
<VisualElement name="modal-icon" class="d-none" />
|
||||
<Label name="modal-title" class="text-bold text-center flex-grow-1" />
|
||||
<Button name="modal-close-btn" text=" " class="pl-2 pr-2" />
|
||||
</VisualElement>
|
||||
<VisualElement name="modal-content-wrapper" class="flex-grow row w-full">
|
||||
<VisualElement name="modal-content" class="flex-grow flex-9 w-full col white-space-normal" />
|
||||
<VisualElement name="modal-action-button-wrapper" class="flex-3 p-3 d-none">
|
||||
<Button name="modal-action-button" class="text-lg text-bold flex-grow-1" />
|
||||
</VisualElement>
|
||||
</VisualElement>
|
||||
</VisualElement>
|
||||
</UXML>
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 240d33b1f1a64e838c456ccacd8e20bd
|
||||
timeCreated: 1732148673
|
||||
@ -0,0 +1,53 @@
|
||||
Modal {
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#modal-backdrop {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#modal-container {
|
||||
margin: auto;
|
||||
max-width: 95%;
|
||||
width: 100%;
|
||||
background-color: var(--unity-colors-window-background);
|
||||
}
|
||||
|
||||
#modal-header {
|
||||
background-color: var(--unity-colors-highlight-background-hover-lighter);
|
||||
}
|
||||
|
||||
#modal-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
#modal-close-btn {
|
||||
border-width: 0;
|
||||
background-color: transparent;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.dark #modal-close-btn {
|
||||
background-image: resource("d_winbtn_win_close");
|
||||
}
|
||||
|
||||
.light #modal-close-btn {
|
||||
background-image: resource("winbtn_win_close");
|
||||
}
|
||||
|
||||
.dark #modal-close-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.light #modal-close-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d3cce9efacf428b90b9d59443ed190c
|
||||
timeCreated: 1732148684
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 187afe4ea3284a31a04e1f301ba845d1
|
||||
timeCreated: 1731711575
|
||||
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
public class OptionsPopupContent: PopupWindowContent
|
||||
{
|
||||
private readonly Action<VisualElement> _setup;
|
||||
private readonly Action _onClose;
|
||||
private readonly Vector2 _windowSize;
|
||||
|
||||
public override Vector2 GetWindowSize()
|
||||
{
|
||||
return _windowSize;
|
||||
}
|
||||
|
||||
public OptionsPopupContent(Action<VisualElement> setup, Vector2 size, Action onClose = null)
|
||||
{
|
||||
_setup = setup;
|
||||
_windowSize = size;
|
||||
_onClose = onClose;
|
||||
}
|
||||
|
||||
public override void OnGUI(Rect rect)
|
||||
{
|
||||
// Legacy stub per unity docs
|
||||
// https://docs.unity3d.com/2022.3/Documentation/Manual/UIE-create-a-popup-window.html
|
||||
}
|
||||
|
||||
private VisualElement _root;
|
||||
|
||||
// Essentially the `CreateGUI` alternative
|
||||
public override void OnOpen()
|
||||
{
|
||||
_root = editorWindow.rootVisualElement;
|
||||
_root.AddToClassList("options-popup-content");
|
||||
|
||||
_root.styleSheets.Add(Resources.Load<StyleSheet>("OptionsPopupFieldStyles"));
|
||||
|
||||
_setup(_root);
|
||||
}
|
||||
|
||||
public override void OnClose()
|
||||
{
|
||||
_onClose?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 40e6521ada904cf7ae415de1134f15c9
|
||||
timeCreated: 1731712258
|
||||
@ -0,0 +1,238 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using PopupWindow = UnityEditor.PopupWindow;
|
||||
|
||||
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
public class OptionsPopupField<T> : VisualElement
|
||||
{
|
||||
#region Child API
|
||||
protected virtual int GetOptionCount()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected virtual IList<T> GetOptions()
|
||||
{
|
||||
return new List<T>();
|
||||
}
|
||||
|
||||
protected virtual string GetOptionName(T option)
|
||||
{
|
||||
return option.ToString();
|
||||
}
|
||||
|
||||
|
||||
protected virtual bool IsOptionLocked(T option)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected virtual string GetPopupButtonText()
|
||||
{
|
||||
return $"{_selectedOptions?.Count ?? 0}/{GetOptionCount()} Selected";
|
||||
}
|
||||
|
||||
protected virtual int GetPopupHeight()
|
||||
{
|
||||
return GetOptionCount() * 26;
|
||||
}
|
||||
#endregion
|
||||
|
||||
private VisualElement _optionsContainer;
|
||||
private Label _label;
|
||||
private readonly Button _popupButton;
|
||||
|
||||
private IList<T> _selectedOptions;
|
||||
public IList<T> SelectedOptions
|
||||
{
|
||||
get => _selectedOptions;
|
||||
set
|
||||
{
|
||||
// Ensure we're not sharing references
|
||||
_selectedOptions = value.ToList();
|
||||
var validOptions = GetOptions();
|
||||
foreach (var option in value)
|
||||
{
|
||||
if (option == null)
|
||||
{
|
||||
_selectedOptions.Remove(option);
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(GetOptionName(option)))
|
||||
{
|
||||
_selectedOptions.Remove(option);
|
||||
continue;
|
||||
}
|
||||
if (!validOptions.Contains(option))
|
||||
{
|
||||
_selectedOptions.Remove(option);
|
||||
}
|
||||
}
|
||||
_popupButton.text = GetPopupButtonText();
|
||||
UpdateOptions(ref _optionsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
private IList<T> _originalOptions = new List<T>();
|
||||
public IList<T> OriginalOptions
|
||||
{
|
||||
get => _originalOptions;
|
||||
set
|
||||
{
|
||||
// Ensure we're not sharing references
|
||||
_originalOptions = value.ToList();
|
||||
var validOptions = GetOptions();
|
||||
foreach (var option in value)
|
||||
{
|
||||
if (option == null)
|
||||
{
|
||||
_originalOptions.Remove(option);
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(GetOptionName(option)))
|
||||
{
|
||||
_originalOptions.Remove(option);
|
||||
continue;
|
||||
}
|
||||
if (!validOptions.Contains(option))
|
||||
{
|
||||
_originalOptions.Remove(option);
|
||||
}
|
||||
}
|
||||
UpdateOptions(ref _optionsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
private bool _loading;
|
||||
|
||||
public bool Loading
|
||||
{
|
||||
get => _loading;
|
||||
set
|
||||
{
|
||||
_loading = value;
|
||||
_popupButton.text = value ? "Loading..." : GetPopupButtonText();
|
||||
}
|
||||
}
|
||||
|
||||
public EventHandler<T> OnToggleOption;
|
||||
public EventHandler<List<T>> OnPopupClosed;
|
||||
private VisualElement _popupBoundsReference;
|
||||
|
||||
public new class UxmlFactory : UxmlFactory<OptionsPopupField<T>, UxmlTraits> { }
|
||||
|
||||
public new class UxmlTraits : VisualElement.UxmlTraits
|
||||
{
|
||||
private readonly UxmlStringAttributeDescription _label = new() { name = "label" };
|
||||
|
||||
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
|
||||
{
|
||||
get { yield break; }
|
||||
}
|
||||
|
||||
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
|
||||
{
|
||||
base.Init(ve, bag, cc);
|
||||
var optionsField = (OptionsPopupField<T>)ve;
|
||||
var label = _label.GetValueFromBag(bag, cc);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(label))
|
||||
optionsField.Q<Label>("label").AddToClassList("d-none");
|
||||
else
|
||||
optionsField.Q<Label>("label").text = label;
|
||||
}
|
||||
}
|
||||
|
||||
public OptionsPopupField()
|
||||
{
|
||||
Resources.Load<VisualTreeAsset>("OptionsPopupField").CloneTree(this);
|
||||
styleSheets.Add(Resources.Load<StyleSheet>("OptionsPopupFieldStyles"));
|
||||
|
||||
var dropdown = this.Q("dropdown");
|
||||
|
||||
_popupButton = new Button
|
||||
{
|
||||
text = "0/5 Selected",
|
||||
name = "popup-button"
|
||||
};
|
||||
|
||||
var spacerElement = new VisualElement
|
||||
{
|
||||
name = "spacer"
|
||||
};
|
||||
|
||||
var arrowElement = new VisualElement
|
||||
{
|
||||
name = "arrow"
|
||||
};
|
||||
|
||||
_popupButton.Add(spacerElement);
|
||||
_popupButton.Add(arrowElement);
|
||||
dropdown.Add(_popupButton);
|
||||
|
||||
_popupButton.clicked += () =>
|
||||
{
|
||||
PopupWindow.Show(_popupButton.worldBound, new OptionsPopupContent(r =>
|
||||
{
|
||||
_optionsContainer = new VisualElement();
|
||||
r.Add(_optionsContainer);
|
||||
UpdateOptions(ref _optionsContainer);
|
||||
}, new Vector2((_popupBoundsReference ?? _popupButton).worldBound.width, GetPopupHeight()), () =>
|
||||
{
|
||||
OnPopupClosed?.Invoke(this, _selectedOptions.ToList());
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
public OptionsPopupField(string label): this()
|
||||
{
|
||||
this.Q<Label>("label").text = label;
|
||||
}
|
||||
|
||||
public void SetPopupBoundsReference(VisualElement reference)
|
||||
{
|
||||
_popupBoundsReference = reference;
|
||||
}
|
||||
|
||||
private VisualElement CreateOption(T option)
|
||||
{
|
||||
var optionElement = new VisualElement();
|
||||
optionElement.AddToClassList("row");
|
||||
optionElement.AddToClassList("option");
|
||||
optionElement.SetEnabled(!IsOptionLocked(option));
|
||||
|
||||
var optionToggle = new Toggle
|
||||
{
|
||||
value = SelectedOptions.Contains(option)
|
||||
};
|
||||
optionToggle.RegisterValueChangedCallback(_ => OnToggleOption?.Invoke(this, option));
|
||||
optionElement.Add(optionToggle);
|
||||
|
||||
var optionText = GetOptionName(option);
|
||||
var optionLabel = new Label(optionText);
|
||||
optionElement.Add(optionLabel);
|
||||
|
||||
return optionElement;
|
||||
}
|
||||
|
||||
private void UpdateOptions(ref VisualElement optionContainer)
|
||||
{
|
||||
if (optionContainer == null)
|
||||
return;
|
||||
|
||||
optionContainer.Clear();
|
||||
foreach (var option in GetOptions())
|
||||
optionContainer.Add(CreateOption(option));
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
var allOptions = GetOptions().ToList();
|
||||
SelectedOptions = SelectedOptions.Where(o => allOptions.Contains(o)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 285e5c57962044cd9d5101bc1ee375c6
|
||||
timeCreated: 1731711666
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5b6808abae04eefbc31e24bf9126229
|
||||
timeCreated: 1731711587
|
||||
@ -0,0 +1,6 @@
|
||||
<UXML xmlns="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
|
||||
<VisualElement class="col align-items-stretch">
|
||||
<Label name="label" class="mt-2 text-bold mb-2" />
|
||||
<VisualElement name="dropdown" class="align-self-stretch" />
|
||||
</VisualElement>
|
||||
</UXML>
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1c4aa2cda4444e694afc677e518ccb0
|
||||
timeCreated: 1731711596
|
||||
@ -0,0 +1,66 @@
|
||||
.row {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.col {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.d-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.options-popup-content
|
||||
{
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
#label {
|
||||
min-width: 135px;
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: 0;
|
||||
margin: 1px 5px 5px 0;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.option Toggle {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
.option Label {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* We need to use a specific name to have these styles override the base USS */
|
||||
#dropdown #popup-button
|
||||
{
|
||||
-unity-text-align: middle-left;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
font-size: 12px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#dropdown Button VisualElement {
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#dropdown Button #spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#dropdown Button #arrow {
|
||||
background-image: resource("d_dropdown.png");
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
align-self: center;
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea3522211f3048049d1bec21d43cbc96
|
||||
timeCreated: 1731711617
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82617318d5c746709bb74ffc8f15ef52
|
||||
timeCreated: 1736370342
|
||||
@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
public class PlatformSwitcherPopup : OptionsPopupField<BuildTarget>
|
||||
{
|
||||
protected override IList<BuildTarget> GetOptions()
|
||||
{
|
||||
return VRC_EditorTools.GetBuildTargetOptionsAsEnum();
|
||||
}
|
||||
|
||||
protected override bool IsOptionLocked(BuildTarget target)
|
||||
{
|
||||
var shouldLock = ShouldLockOption?.Invoke(target) ?? false;
|
||||
return shouldLock || !VRC_EditorTools.IsBuildTargetSupported(target);
|
||||
}
|
||||
|
||||
protected override int GetOptionCount()
|
||||
{
|
||||
return GetOptions().Count;
|
||||
}
|
||||
|
||||
protected override string GetOptionName(BuildTarget target)
|
||||
{
|
||||
return VRC_EditorTools.GetTargetName(target);
|
||||
}
|
||||
|
||||
protected override string GetPopupButtonText()
|
||||
{
|
||||
if (SelectedOptions.Count == 1)
|
||||
{
|
||||
return GetOptionName(SelectedOptions[0]);
|
||||
}
|
||||
|
||||
if (SelectedOptions.Count == 0)
|
||||
{
|
||||
return "None Selected";
|
||||
}
|
||||
|
||||
if (SelectedOptions.Count == GetOptionCount())
|
||||
{
|
||||
return "All Platforms";
|
||||
}
|
||||
|
||||
return $"{SelectedOptions?.Count ?? 0}/{GetOptionCount()} Selected";
|
||||
}
|
||||
|
||||
public new class UxmlFactory : UxmlFactory<PlatformSwitcherPopup, UxmlTraits> { }
|
||||
public new class UxmlTraits : OptionsPopupField<BuildTarget>.UxmlTraits { }
|
||||
|
||||
public Func<BuildTarget, bool> ShouldLockOption { get; set; }
|
||||
|
||||
public PlatformSwitcherPopup()
|
||||
{}
|
||||
|
||||
public PlatformSwitcherPopup(string label): base(label)
|
||||
{}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b36b8f84f1234cb09558f2b7bc4a8a56
|
||||
timeCreated: 1736370358
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2642084c16904ccd93071a1f2aca7247
|
||||
timeCreated: 1745315447
|
||||
@ -0,0 +1,117 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEngine.UIElements.Experimental;
|
||||
|
||||
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
public abstract class Selector<T> : VisualElement
|
||||
{
|
||||
public bool PopupEnabled
|
||||
{
|
||||
get => _popupField.enabledSelf;
|
||||
set => _popupField.SetEnabled(value);
|
||||
}
|
||||
|
||||
private readonly string _popupFieldName;
|
||||
private readonly bool _pingWhenOptionsSet;
|
||||
private PopupField<T> _popupField;
|
||||
private VisualElement _popupInput;
|
||||
private EventCallback<ChangeEvent<T>> _changeCallback;
|
||||
|
||||
protected Selector(List<T> options, string popupFieldName, string styleSheetPath, string labelText, string labelName, bool pingWhenOptionsSet)
|
||||
{
|
||||
_popupFieldName = popupFieldName;
|
||||
_pingWhenOptionsSet = pingWhenOptionsSet;
|
||||
|
||||
styleSheets.Add(Resources.Load<StyleSheet>(styleSheetPath));
|
||||
var label = new Label(labelText)
|
||||
{
|
||||
name = labelName
|
||||
};
|
||||
Add(label);
|
||||
SetOptions(options, 0);
|
||||
}
|
||||
|
||||
private void CreateField(List<T> options, int selectedIndex)
|
||||
{
|
||||
if (Contains(_popupField))
|
||||
{
|
||||
Remove(_popupField);
|
||||
}
|
||||
|
||||
if (options == null || options.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_popupField = new PopupField<T>(
|
||||
null,
|
||||
options,
|
||||
selectedIndex,
|
||||
prop => FormatElementName(options, prop),
|
||||
prop => FormatElementName(options, prop)
|
||||
);
|
||||
_popupInput = _popupField.Q<VisualElement>(null, "unity-popup-field__input");
|
||||
_popupField.name = _popupFieldName;
|
||||
_popupField.AddToClassList("flex-grow-1");
|
||||
if (_changeCallback != null)
|
||||
{
|
||||
_popupField.RegisterValueChangedCallback(_changeCallback);
|
||||
}
|
||||
Add(_popupField);
|
||||
}
|
||||
|
||||
protected abstract string FormatElementName(List<T> options, T element);
|
||||
|
||||
public void SetOptions(List<T> options, int selectedIndex)
|
||||
{
|
||||
CreateField(options, selectedIndex);
|
||||
if (_pingWhenOptionsSet)
|
||||
{
|
||||
PingField();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetValue(T element, bool setWithoutNotify = false)
|
||||
{
|
||||
if (_popupField == null) return;
|
||||
|
||||
if (setWithoutNotify)
|
||||
{
|
||||
_popupField.SetValueWithoutNotify(element);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popupField.value = element;
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterValueChangedCallback(EventCallback<ChangeEvent<T>> callback)
|
||||
{
|
||||
_changeCallback = callback;
|
||||
if (_changeCallback != null)
|
||||
{
|
||||
_popupField.RegisterValueChangedCallback(_changeCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private void PingField()
|
||||
{
|
||||
if (_popupField == null) return;
|
||||
_popupField.schedule.Execute(() =>
|
||||
{
|
||||
var baseColor = _popupInput.resolvedStyle.backgroundColor;
|
||||
_popupInput.experimental.animation.Start(new StyleValues
|
||||
{
|
||||
backgroundColor = new Color(0.3f, 0.71f, 0.37f, 0.53f)
|
||||
}, new StyleValues
|
||||
{
|
||||
backgroundColor = baseColor
|
||||
}, 500);
|
||||
}).ExecuteLater(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa3fe294802346828d8fca51c0f8eed6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d8e845dfe774648a8e66f92e190671d
|
||||
timeCreated: 1733441765
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b7fc41b9c6234e7b994e0c7d92839625
|
||||
timeCreated: 1733442419
|
||||
@ -0,0 +1,6 @@
|
||||
/* There is a higher level parent stylesheet that we want to override with a more specific selector */
|
||||
.section-foldout > Toggle Label.step-label,
|
||||
.step-label{
|
||||
flex-grow: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12073cbba57b41bea0300fb13645ebec
|
||||
timeCreated: 1733442413
|
||||
@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
public class StepFoldout: Foldout
|
||||
{
|
||||
public new class UxmlFactory : UxmlFactory<StepFoldout, UxmlTraits> {}
|
||||
|
||||
public new class UxmlTraits : Foldout.UxmlTraits
|
||||
{
|
||||
private readonly UxmlStringAttributeDescription _stepName = new() { name = "stepName" };
|
||||
|
||||
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
|
||||
{
|
||||
get { yield break; }
|
||||
}
|
||||
|
||||
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
|
||||
{
|
||||
base.Init(ve, bag, cc);
|
||||
var stepFoldout = (StepFoldout) ve;
|
||||
stepFoldout.InsertStepName(_stepName.GetValueFromBag(bag, cc));
|
||||
}
|
||||
}
|
||||
|
||||
private string _stepName;
|
||||
private Label _headerLabel;
|
||||
|
||||
private void InsertStepName(string stepName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stepName)) return;
|
||||
_stepName = stepName;
|
||||
|
||||
|
||||
// We insert the step right before the main label
|
||||
_headerLabel = this.Q<Toggle>().Q<Label>();
|
||||
var headerIndex = _headerLabel.parent.hierarchy.IndexOf(_headerLabel);
|
||||
var stepLabel = new Label(_stepName);
|
||||
stepLabel.AddToClassList("step-label");
|
||||
_headerLabel.parent.Insert(headerIndex, stepLabel);
|
||||
}
|
||||
|
||||
public StepFoldout()
|
||||
{
|
||||
styleSheets.Add(Resources.Load<StyleSheet>("StepFoldoutStyles"));
|
||||
}
|
||||
|
||||
public void SetTitle(string title)
|
||||
{
|
||||
_headerLabel.text = title;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9cb09dda70d84125b7d55497e3e7f6b0
|
||||
timeCreated: 1733441771
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f921025be084aacbbc9d54d0c3a0cee
|
||||
timeCreated: 1684525016
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8fb982a3dd74475901ec426a5cbc815
|
||||
timeCreated: 1687270524
|
||||
@ -0,0 +1,17 @@
|
||||
<UXML xmlns="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns:vrc="VRC.SDKBase.Editor.Elements">
|
||||
<VisualElement class="col">
|
||||
<Label name="tags-label" class="text-bold mb-2" />
|
||||
<vrc:TagsFieldButton class="align-self-stretch row" />
|
||||
<vrc:Modal name="tags-modal" title="Manage Your Tags">
|
||||
<VisualElement class="col">
|
||||
<VisualElement class="row p-4" name="add-tag-block">
|
||||
<vrc:VRCTextField name="tag-add-field" class="flex-grow-1" placeholder="Enter a new tag..." />
|
||||
<Button name="tag-add-button" text="Add Tag" />
|
||||
</VisualElement>
|
||||
<ScrollView>
|
||||
<VisualElement name="tags-row" class="row pl-4 pr-4 pt-3 pb-2" />
|
||||
</ScrollView>
|
||||
</VisualElement>
|
||||
</vrc:Modal>
|
||||
</VisualElement>
|
||||
</UXML>
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36d3f6fd312f4ef48aa0cd0b8fa0c9ef
|
||||
timeCreated: 1684525036
|
||||
@ -0,0 +1,82 @@
|
||||
#tags-row {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 2px 3px 2px 7px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
-unity-text-align: middle-center;
|
||||
flex-shrink: 0;
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
.dark #add-tag-block {
|
||||
background-color: hsl(0, 0%, 19%);
|
||||
}
|
||||
|
||||
.light #add-tag-block {
|
||||
background-color: hsl(0, 0%, 39%);
|
||||
}
|
||||
|
||||
|
||||
.light .tag {
|
||||
background-color: rgb(228, 228, 228);
|
||||
}
|
||||
|
||||
.dark .tag {
|
||||
background-color: hsl(0, 0%, 18%);
|
||||
}
|
||||
|
||||
.tag Button.tag-remove-button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-width: 0;
|
||||
background-color: rgba(0,0,0,0);
|
||||
padding-left: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
opacity: 0.5;
|
||||
cursor: link;
|
||||
}
|
||||
|
||||
.dark .tag Button.tag-remove-button {
|
||||
background-image: resource("d_winbtn_win_close");
|
||||
}
|
||||
|
||||
.light .tag Button.tag-remove-button {
|
||||
background-image: resource("winbtn_win_close");
|
||||
}
|
||||
|
||||
.tag Button.tag-remove-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#tags-label {
|
||||
min-width: 135px;
|
||||
}
|
||||
|
||||
TagsFieldButton {
|
||||
-unity-text-align: middle-left;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
TagsFieldButton VisualElement {
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
TagsFieldButton #spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
TagsFieldButton #edit {
|
||||
background-image: resource("vrcTagEditIcon");
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
align-self: center;
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf83c9f55a1e4586b639fab90fcc48dc
|
||||
timeCreated: 1684525082
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 560 B |
@ -0,0 +1,140 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15deb94ecaaa9de4f85099145052175a
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 12
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: iPhone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Android
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,172 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
public class TagsField: VisualElement
|
||||
{
|
||||
private Label _tagsLabel;
|
||||
private TagsFieldButton _tagsButton;
|
||||
private Modal _tagsModal;
|
||||
private VisualElement _tagsRow;
|
||||
private Button _addTagButton;
|
||||
private VRCTextField _tagInput;
|
||||
|
||||
private List<string> _tags;
|
||||
public IList<string> tags
|
||||
{
|
||||
get => _tags;
|
||||
set
|
||||
{
|
||||
var copied = new List<string>(value);
|
||||
if (TagFilter != null)
|
||||
{
|
||||
copied = TagFilter(copied);
|
||||
}
|
||||
_tags = copied;
|
||||
_tagsButton?.SetTagCount(_tags.Count);
|
||||
UpdateTags(ref _tagsRow);
|
||||
_tagsModal?.SetTitle($"Manage Your Tags ({copied.Count})");
|
||||
}
|
||||
}
|
||||
|
||||
public EventHandler<string> OnAddTag;
|
||||
public EventHandler<string> OnRemoveTag;
|
||||
public Func<bool> CanAddTag;
|
||||
public Func<string, bool> IsProtectedTag = input => false;
|
||||
public Func<string, string> FormatTagDisplay = input => input;
|
||||
public Func<List<string>, List<string>> TagFilter;
|
||||
public int TagLimit = 5;
|
||||
|
||||
public new class UxmlFactory : UxmlFactory<TagsField, UxmlTraits> {}
|
||||
|
||||
public new class UxmlTraits : VisualElement.UxmlTraits
|
||||
{
|
||||
private UxmlStringAttributeDescription _label = new UxmlStringAttributeDescription { name = "label" };
|
||||
|
||||
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
|
||||
{
|
||||
get { yield break; }
|
||||
}
|
||||
|
||||
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
|
||||
{
|
||||
base.Init(ve, bag, cc);
|
||||
var tagsField = (TagsField) ve;
|
||||
var label = _label.GetValueFromBag(bag, cc);
|
||||
if (string.IsNullOrWhiteSpace(label))
|
||||
{
|
||||
tagsField.Q<Label>("tags-label").AddToClassList("d-none");
|
||||
}
|
||||
else
|
||||
{
|
||||
tagsField.Q<Label>("tags-label").text = label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TagsField()
|
||||
{
|
||||
Resources.Load<VisualTreeAsset>("TagsField").CloneTree(this);
|
||||
styleSheets.Add(Resources.Load<StyleSheet>("TagsFieldStyles"));
|
||||
|
||||
_tagsLabel = this.Q<Label>("tags-label");
|
||||
_tagsButton = this.Q<TagsFieldButton>();
|
||||
_tagsRow = this.Q("tags-row");
|
||||
_tagsModal = this.Q<Modal>("tags-modal");
|
||||
_tagsButton.clicked += _tagsModal.Open;
|
||||
_tagsModal.styleSheets.Add(Resources.Load<StyleSheet>("TagsFieldStyles"));
|
||||
|
||||
_tagInput = _tagsModal.Q<VRCTextField>("tag-add-field");
|
||||
_tagInput.RegisterCallback<KeyDownEvent>(e =>
|
||||
{
|
||||
if (e.keyCode != KeyCode.Return) return;
|
||||
AddTag();
|
||||
});
|
||||
|
||||
// Comma is a valid input event, so adding a tag and clearing input on KeyDown causes internal errors
|
||||
// so we do it on key up and trim the end
|
||||
_tagInput.RegisterCallback<KeyUpEvent>(e =>
|
||||
{
|
||||
if (e.keyCode != KeyCode.Comma) return;
|
||||
_tagInput.value = _tagInput.value[..^1];
|
||||
AddTag();
|
||||
});
|
||||
_addTagButton = _tagsModal.Q<Button>("tag-add-button");
|
||||
_addTagButton.clicked += AddTag;
|
||||
|
||||
tags = new List<string>();
|
||||
|
||||
// Anchor the modal to the content-info block
|
||||
RegisterCallback<AttachToPanelEvent>(e =>
|
||||
{
|
||||
_tagsModal.SetAnchor(e.destinationPanel.visualTree.Q("content-info"));
|
||||
});
|
||||
}
|
||||
|
||||
public TagsField(List<string> tags) : this()
|
||||
{
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the editing of the tags list, closes the modal and clears the input field
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public void StopEditing()
|
||||
{
|
||||
_tagInput.value = string.Empty;
|
||||
_tagsModal.Close();
|
||||
}
|
||||
|
||||
private void AddTag()
|
||||
{
|
||||
if (tags.Count >= TagLimit)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (CanAddTag != null && !CanAddTag())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_tagInput.IsPlaceholder()) return;
|
||||
|
||||
if (_tagInput.value.Length == 0) return;
|
||||
|
||||
OnAddTag?.Invoke(this, _tagInput.text);
|
||||
_tagInput.value = string.Empty;
|
||||
}
|
||||
|
||||
private void UpdateTags(ref VisualElement container)
|
||||
{
|
||||
container.Clear();
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
var tagElement = new VisualElement();
|
||||
tagElement.AddToClassList("tag");
|
||||
tagElement.AddToClassList("row");
|
||||
tagElement.AddToClassList("mr-2");
|
||||
tagElement.AddToClassList("mb-2");
|
||||
|
||||
tagElement.Add(new Label(FormatTagDisplay(tag)));
|
||||
if (!IsProtectedTag(tag))
|
||||
{
|
||||
var removeButton = new Button(() =>
|
||||
{
|
||||
OnRemoveTag?.Invoke(this, tag);
|
||||
});
|
||||
removeButton.AddToClassList("tag-remove-button");
|
||||
tagElement.Add(removeButton);
|
||||
}
|
||||
container.Add(tagElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cefc8575eca3497fbe2221e89b6ba018
|
||||
timeCreated: 1684525020
|
||||
@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
|
||||
public class TagsFieldButton: Button
|
||||
{
|
||||
public new class UxmlFactory : UxmlFactory<TagsFieldButton, UxmlTraits> {}
|
||||
public new class UxmlTraits : VisualElement.UxmlTraits
|
||||
{
|
||||
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
|
||||
{
|
||||
get { yield break; }
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTagCount(int count)
|
||||
{
|
||||
text = $"{count} Tag(s)";
|
||||
}
|
||||
|
||||
public TagsFieldButton() : base()
|
||||
{
|
||||
var spacerElement = new VisualElement
|
||||
{
|
||||
name = "spacer"
|
||||
};
|
||||
var editElement = new VisualElement
|
||||
{
|
||||
name = "edit"
|
||||
};
|
||||
|
||||
Add(spacerElement);
|
||||
Add(editElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87a08313d01a4f00b1307526761a64f9
|
||||
timeCreated: 1736209770
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a2a716fec9d4f7a94426c04f827c060
|
||||
timeCreated: 1687701391
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6626c41f1b84ccb95dc502c72e6b902
|
||||
timeCreated: 1687701682
|
||||
@ -0,0 +1,11 @@
|
||||
<UXML xmlns="UnityEngine.UIElements">
|
||||
<VisualElement class="thumbnail" name="thumbnail-container">
|
||||
<VisualElement class="thumbnail-underlay">
|
||||
<Label name="thumbnail-placeholder-text" />
|
||||
</VisualElement>
|
||||
<VisualElement name="thumbnail-image" />
|
||||
<VisualElement class="thumbnail-overlay">
|
||||
<Label name="thumbnail-hover-text" />
|
||||
</VisualElement>
|
||||
</VisualElement>
|
||||
</UXML>
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f436adf433bd48d69a078c8ec49814e0
|
||||
timeCreated: 1687701705
|
||||
@ -0,0 +1,56 @@
|
||||
.thumbnail {
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.thumbnail-underlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
-unity-text-align: middle-center;
|
||||
-unity-font-style: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.thumbnail:disabled .thumbnail-underlay #thumbnail-placeholder-text {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#thumbnail-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
-unity-background-scale-mode: scale-and-crop;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.thumbnail-overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
opacity: 0;
|
||||
-unity-text-align: middle-center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.thumbnail-overlay Label {
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
.thumbnail:hover .thumbnail-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f774b221f3fa4973a0931098da26177e
|
||||
timeCreated: 1687702140
|
||||
@ -0,0 +1,176 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using VRC.SDKBase.Editor.Api;
|
||||
|
||||
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
public class Thumbnail: VisualElement
|
||||
{
|
||||
public new class UxmlFactory : UxmlFactory<Thumbnail, UxmlTraits> {}
|
||||
|
||||
public new class UxmlTraits : VisualElement.UxmlTraits
|
||||
{
|
||||
private readonly UxmlFloatAttributeDescription _width = new UxmlFloatAttributeDescription { name = "width" };
|
||||
private readonly UxmlFloatAttributeDescription _height = new UxmlFloatAttributeDescription { name = "height" };
|
||||
private readonly UxmlStringAttributeDescription _placeholder = new UxmlStringAttributeDescription { name = "placeholder" };
|
||||
private readonly UxmlStringAttributeDescription _loadingText = new UxmlStringAttributeDescription { name = "loading-text" };
|
||||
private readonly UxmlStringAttributeDescription _hoverText = new UxmlStringAttributeDescription { name = "hover-text" };
|
||||
private readonly UxmlStringAttributeDescription _imageUrl = new UxmlStringAttributeDescription { name = "image-url" };
|
||||
|
||||
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
|
||||
{
|
||||
get { yield break; }
|
||||
}
|
||||
|
||||
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
|
||||
{
|
||||
base.Init(ve, bag, cc);
|
||||
var thumbnail = (Thumbnail) ve;
|
||||
var width = 0f;
|
||||
if (_width.TryGetValueFromBag(bag, cc, ref width))
|
||||
{
|
||||
thumbnail._width = width;
|
||||
}
|
||||
var height = 0f;
|
||||
if (_height.TryGetValueFromBag(bag, cc, ref height))
|
||||
{
|
||||
thumbnail._height = height;
|
||||
}
|
||||
|
||||
var placeholder = "";
|
||||
if (_placeholder.TryGetValueFromBag(bag, cc, ref placeholder))
|
||||
{
|
||||
thumbnail._placeholder = placeholder;
|
||||
}
|
||||
var loadingText = "";
|
||||
if (_loadingText.TryGetValueFromBag(bag, cc, ref loadingText))
|
||||
{
|
||||
thumbnail._loadingText = loadingText;
|
||||
}
|
||||
var hoverText = "";
|
||||
if (_hoverText.TryGetValueFromBag(bag, cc, ref hoverText))
|
||||
{
|
||||
thumbnail._hoverText = hoverText;
|
||||
}
|
||||
thumbnail.UpdateProps();
|
||||
var imageUrl = "";
|
||||
if (_imageUrl.TryGetValueFromBag(bag, cc, ref imageUrl))
|
||||
{
|
||||
thumbnail.SetImageUrl(imageUrl).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float _width = 192f;
|
||||
private float _height = 144f;
|
||||
private string _placeholder = "No Image";
|
||||
private string _loadingText = "Loading...";
|
||||
private string _hoverText = "Image Size\n(1200 x 900)";
|
||||
private readonly VisualElement _container;
|
||||
private readonly Label _placeholderText;
|
||||
private readonly Label _hoverTextElement;
|
||||
private readonly VisualElement _imageElement;
|
||||
private string _imageUrl;
|
||||
private Texture2D _imageTexture;
|
||||
private Texture2D _transparentPlaceholder;
|
||||
|
||||
public string CurrentImage => _imageUrl;
|
||||
public Texture2D CurrentImageTexture => _imageTexture;
|
||||
|
||||
private bool _loading;
|
||||
|
||||
public bool Loading
|
||||
{
|
||||
get => _loading;
|
||||
set
|
||||
{
|
||||
_loading = value;
|
||||
_placeholderText.text = _loading ? _loadingText : _placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
public Thumbnail()
|
||||
{
|
||||
Resources.Load<VisualTreeAsset>("Thumbnail").CloneTree(this);
|
||||
styleSheets.Add(Resources.Load<StyleSheet>("ThumbnailStyles"));
|
||||
|
||||
_container = this.Q("thumbnail-container");
|
||||
_placeholderText = this.Q<Label>("thumbnail-placeholder-text");
|
||||
_hoverTextElement = this.Q<Label>("thumbnail-hover-text");
|
||||
_imageElement = this.Q("thumbnail-image");
|
||||
|
||||
_container.style.width = _width;
|
||||
_container.style.height = _height;
|
||||
_container.style.minWidth = _width;
|
||||
|
||||
_placeholderText.text = _placeholder;
|
||||
_hoverTextElement.text = _hoverText;
|
||||
|
||||
_transparentPlaceholder = new Texture2D(1, 1);
|
||||
_transparentPlaceholder.SetPixel(0, 0, new Color(0,0,0,0));
|
||||
_transparentPlaceholder.Apply();
|
||||
}
|
||||
|
||||
private void UpdateProps()
|
||||
{
|
||||
_container.style.width = _width;
|
||||
_container.style.height = _height;
|
||||
_container.style.minWidth = _width;
|
||||
|
||||
_placeholderText.text = _placeholder;
|
||||
_hoverTextElement.text = _hoverText;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task SetImageUrl(string url, CancellationToken cancellationToken = default, bool forceRefresh = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return;
|
||||
if (url == _imageUrl) return;
|
||||
_imageUrl = url;
|
||||
_imageTexture = null;
|
||||
Loading = true;
|
||||
try
|
||||
{
|
||||
_imageElement.style.backgroundImage = await VRCApi.GetImage(url, forceRefresh, cancellationToken: cancellationToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
_imageUrl = null;
|
||||
_imageElement.style.backgroundImage = null;
|
||||
}
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public void SetImage(Texture2D image)
|
||||
{
|
||||
_imageUrl = null;
|
||||
_imageTexture = image;
|
||||
_imageElement.style.backgroundImage = image;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public void SetImage(string imagePath)
|
||||
{
|
||||
var bytes = File.ReadAllBytes(imagePath);
|
||||
var newThumbnail = new Texture2D(2, 2);
|
||||
newThumbnail.LoadImage(bytes);
|
||||
SetImage(newThumbnail);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public void ClearImage()
|
||||
{
|
||||
_imageUrl = null;
|
||||
_imageTexture = null;
|
||||
_imageElement.style.backgroundImage = _transparentPlaceholder;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21784b55f82d46e0aba5b8fec85c5551
|
||||
timeCreated: 1687701396
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 929f1b0665a749a5bceb45abb7b3cb8b
|
||||
timeCreated: 1731031495
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96d38749df544cc59b75494732690ae4
|
||||
timeCreated: 1731031558
|
||||
@ -0,0 +1,34 @@
|
||||
<UXML xmlns="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns:vrc="VRC.SDKBase.Editor.Elements" xmlns:vrca="VRC.SDK3A.Editor.Elements">
|
||||
<VisualElement class="col mb-2 align-items-start w-100">
|
||||
<vrc:Thumbnail width="196" height="147" hover-text="Image Size 1200 x 900" name="content-thumbnail" />
|
||||
<VisualElement class="row align-items-stretch justify-content-between mt-2" style="max-width: 100%; min-width:100%;">
|
||||
<Button text="Select Image" name="select-new-thumbnail-btn" class="ml-0 mr-2 white-space-normal text-bold pt-2 pb-2" style="flex:1; font-size:14px;" />
|
||||
<Button text="Capture In Scene" name="capture-thumbnail-from-scene-btn" class="ml-0 mr-0 white-space-normal text-bold pt-2 pb-2" style="flex:1; font-size:14px;" />
|
||||
</VisualElement>
|
||||
<vrc:Modal title="Move your scene view to capture the desired thumbnail">
|
||||
<VisualElement class="col">
|
||||
<VisualElement class="row p-3 align-items-center">
|
||||
<VisualElement class="col flex-grow-1" style="min-width: 180px;">
|
||||
<Toggle label="Fill Background" tooltip="Uses flat color as a background instead of the skybox" name="thumbnail-fill-background-toggle" class="mb-2" />
|
||||
<Toggle label="Use PostProcessing" tooltip="Applies scene post processing to the thumbnail" name="thumbnail-use-post-processing-toggle" class="mb-2" />
|
||||
<Toggle label="Use Custom Camera" tooltip="Utilizes a custom camera instead of a VRChat-created camera instead" name="thumbnail-use-custom-camera-toggle" class="mb-2" />
|
||||
<VisualElement class="col mb-2 d-none" name="thumbnail-background-block">
|
||||
<uie:ColorField name="thumbnail-background-color-field" class="mb-2" />
|
||||
<Label text="Background Color" style="align-self: center text-center" />
|
||||
</VisualElement>
|
||||
<VisualElement class="col mb-2 d-none" name="thumbnail-custom-camera-block">
|
||||
<uie:ObjectField class="mb-2" name="thumbnail-custom-camera-ref"/>
|
||||
<Label text="Custom Camera" style="align-self: center text-center" />
|
||||
</VisualElement>
|
||||
</VisualElement>
|
||||
<VisualElement class="row justify-content-center mt-2 ml-2 flex-shrink-0 flex-grow-0" style="flex-basis: auto;">
|
||||
<IMGUIContainer name="thumbnail-capture-preview" />
|
||||
</VisualElement>
|
||||
</VisualElement>
|
||||
<VisualElement class="mt-1 mb-3 ml-3 mr-3 row align-items-center" name="thumbnail-capture-confirm-block">
|
||||
<Button class="p-2 m-0 flex-grow-1" text="Capture" name="thumbnail-capture-confirm-btn" style="height: 30px;" />
|
||||
</VisualElement>
|
||||
</VisualElement>
|
||||
</vrc:Modal>
|
||||
</VisualElement>
|
||||
</UXML>
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 123b380a0d8e48f2bfbd20b14d940662
|
||||
timeCreated: 1731031567
|
||||
@ -0,0 +1,267 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.UIElements;
|
||||
using Object = UnityEngine.Object;
|
||||
#if POST_PROCESSING_INCLUDED
|
||||
using UnityEngine.Rendering.PostProcessing;
|
||||
#endif
|
||||
|
||||
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
public class ThumbnailBlock: VisualElement
|
||||
{
|
||||
public new class UxmlFactory : UxmlFactory<ThumbnailBlock, UxmlTraits> {}
|
||||
public new class UxmlTraits : VisualElement.UxmlTraits
|
||||
{
|
||||
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
|
||||
{
|
||||
get { yield break; }
|
||||
}
|
||||
}
|
||||
|
||||
public Thumbnail Thumbnail { get; private set; }
|
||||
public Foldout Foldout { get; private set; }
|
||||
public EventHandler<string> OnNewThumbnailSelected { get; set; }
|
||||
|
||||
private VisualElement _selectorBlock;
|
||||
private Button _selectThumbnailButton;
|
||||
private Button _captureThumbnailButton;
|
||||
private VisualElement _modalMountPoint;
|
||||
|
||||
private VisualElement _captureBlock;
|
||||
private IMGUIContainer _previewContainer;
|
||||
private Toggle _fillBackground;
|
||||
private ColorField _backgroundColor;
|
||||
private Toggle _usePostProcessing;
|
||||
private Toggle _useCustomCamera;
|
||||
private ObjectField _customCamera;
|
||||
private VisualElement _captureConfirmBlock;
|
||||
private Button _captureConfirmButton;
|
||||
private Button _captureCancelButton;
|
||||
private Modal _captureModal;
|
||||
|
||||
private string _oldThumbnail;
|
||||
private Texture2D _oldThumbnailTexture;
|
||||
private Camera _captureCamera;
|
||||
private Texture2D _bufferTexture;
|
||||
private RenderTexture _targetTexture;
|
||||
|
||||
private bool _capturing;
|
||||
private bool Capturing
|
||||
{
|
||||
get => _capturing;
|
||||
set
|
||||
{
|
||||
_capturing = value;
|
||||
if (value)
|
||||
{
|
||||
_captureModal.Open();
|
||||
_targetTexture = Resources.Load<RenderTexture>("ThumbnailCapture");
|
||||
_captureCamera =
|
||||
VRC_EditorTools.CreateThumbnailCaptureCamera(_targetTexture, _fillBackground.value, _backgroundColor.value, _usePostProcessing.value);
|
||||
_captureCamera.enabled = true;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
_captureModal.Close();
|
||||
}
|
||||
if (_captureCamera != null)
|
||||
{
|
||||
Object.DestroyImmediate(_captureCamera.gameObject);
|
||||
_captureCamera = null;
|
||||
}
|
||||
|
||||
if (_customCamera.value != null)
|
||||
{
|
||||
((Camera) _customCamera.value).targetTexture = null;
|
||||
}
|
||||
|
||||
if (_bufferTexture != null)
|
||||
{
|
||||
Object.DestroyImmediate(_bufferTexture);
|
||||
_bufferTexture = null;
|
||||
}
|
||||
|
||||
_targetTexture = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async void HandleCaptureCancel()
|
||||
{
|
||||
Capturing = false;
|
||||
Thumbnail.ClearImage();
|
||||
if (!string.IsNullOrWhiteSpace(_oldThumbnail))
|
||||
{
|
||||
await Thumbnail.SetImageUrl(_oldThumbnail);
|
||||
}
|
||||
|
||||
if (_oldThumbnailTexture != null)
|
||||
{
|
||||
Thumbnail.SetImage(_oldThumbnailTexture);
|
||||
}
|
||||
|
||||
_oldThumbnail = null;
|
||||
_oldThumbnailTexture = null;
|
||||
}
|
||||
|
||||
public ThumbnailBlock()
|
||||
{
|
||||
Resources.Load<VisualTreeAsset>("ThumbnailBlock").CloneTree(this);
|
||||
|
||||
Thumbnail = this.Q<Thumbnail>("content-thumbnail");
|
||||
|
||||
_selectorBlock = this.Q<VisualElement>("thumbnail-selector-block");
|
||||
_selectThumbnailButton = this.Q<Button>("select-new-thumbnail-btn");
|
||||
_captureThumbnailButton = this.Q<Button>("capture-thumbnail-from-scene-btn");
|
||||
|
||||
_captureModal = this.Q<Modal>();
|
||||
_captureModal.OnClose += (_, _) =>
|
||||
{
|
||||
if (!_capturing) return;
|
||||
HandleCaptureCancel();
|
||||
};
|
||||
|
||||
// We cannot traverse the tree until the component is mounted
|
||||
RegisterCallback<AttachToPanelEvent>(evt =>
|
||||
{
|
||||
// the panel is the root of the window tree, so we can see all elements from here
|
||||
_captureModal.SetAnchor(evt.destinationPanel.visualTree.Q("content-info"));
|
||||
});
|
||||
|
||||
_previewContainer = this.Q<IMGUIContainer>("thumbnail-capture-preview");
|
||||
|
||||
_fillBackground = this.Q<Toggle>("thumbnail-fill-background-toggle");
|
||||
var backgroundBlock = this.Q("thumbnail-background-block");
|
||||
_backgroundColor = this.Q<ColorField>("thumbnail-background-color-field");
|
||||
_usePostProcessing = this.Q<Toggle>("thumbnail-use-post-processing-toggle");
|
||||
var customCameraBlock = this.Q("thumbnail-custom-camera-block");
|
||||
_useCustomCamera = this.Q<Toggle>("thumbnail-use-custom-camera-toggle");
|
||||
_customCamera = this.Q<ObjectField>("thumbnail-custom-camera-ref");
|
||||
_captureConfirmBlock = this.Q<VisualElement>("thumbnail-capture-confirm-block");
|
||||
_captureConfirmButton = this.Q<Button>("thumbnail-capture-confirm-btn");
|
||||
_captureCancelButton = this.Q<Button>("thumbnail-capture-cancel-btn");
|
||||
|
||||
#if POST_PROCESSING_INCLUDED
|
||||
_usePostProcessing.RemoveFromClassList("d-none");
|
||||
#endif
|
||||
|
||||
_selectThumbnailButton.clicked += () =>
|
||||
{
|
||||
var imagePath = EditorUtility.OpenFilePanel("Select thumbnail", "", "png");
|
||||
if (string.IsNullOrWhiteSpace(imagePath)) return;
|
||||
OnNewThumbnailSelected?.Invoke(this, imagePath);
|
||||
};
|
||||
|
||||
_captureThumbnailButton.clicked += () =>
|
||||
{
|
||||
_oldThumbnail = Thumbnail.CurrentImage;
|
||||
_oldThumbnailTexture = Thumbnail.CurrentImageTexture;
|
||||
Capturing = true;
|
||||
};
|
||||
|
||||
_captureConfirmButton.clicked += () =>
|
||||
{
|
||||
Capturing = false;
|
||||
var capturedPicture = VRC_EditorTools.CaptureSceneImage(1200, 900, _fillBackground.value,
|
||||
_backgroundColor.value, _usePostProcessing.value,
|
||||
_useCustomCamera.value ? (Camera) _customCamera.value : null);
|
||||
OnNewThumbnailSelected?.Invoke(this, capturedPicture);
|
||||
};
|
||||
|
||||
_fillBackground.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (!Capturing || _captureCamera == null) return;
|
||||
backgroundBlock.EnableInClassList("d-none", !evt.newValue);
|
||||
if (evt.newValue)
|
||||
{
|
||||
_captureCamera.clearFlags = CameraClearFlags.SolidColor;
|
||||
_captureCamera.backgroundColor = _backgroundColor.value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_captureCamera.clearFlags = CameraClearFlags.Skybox;
|
||||
}
|
||||
});
|
||||
|
||||
_backgroundColor.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (!Capturing || _captureCamera == null) return;
|
||||
// Enforce alpha to 1
|
||||
_backgroundColor.SetValueWithoutNotify(new Color(evt.newValue.r, evt.newValue.g, evt.newValue.b, 1f));
|
||||
_captureCamera.backgroundColor = _backgroundColor.value;
|
||||
});
|
||||
|
||||
_usePostProcessing.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
#if POST_PROCESSING_INCLUDED
|
||||
if (!Capturing || _captureCamera == null) return;
|
||||
if (_captureCamera.TryGetComponent<PostProcessLayer>(out var layer))
|
||||
{
|
||||
layer.enabled = evt.newValue;
|
||||
}
|
||||
else if (evt.newValue)
|
||||
{
|
||||
var postProcessLayer = _captureCamera.gameObject.AddComponent<PostProcessLayer>();
|
||||
postProcessLayer.volumeLayer = int.MaxValue;
|
||||
postProcessLayer.volumeTrigger = _captureCamera.transform;
|
||||
}
|
||||
#endif
|
||||
});
|
||||
|
||||
_useCustomCamera.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (!Capturing) return;
|
||||
customCameraBlock.EnableInClassList("d-none", !evt.newValue);
|
||||
});
|
||||
|
||||
_customCamera.objectType = typeof(Camera);
|
||||
_customCamera.allowSceneObjects = true;
|
||||
|
||||
_previewContainer.style.width = 192;
|
||||
_previewContainer.style.height = 144;
|
||||
|
||||
_previewContainer.onGUIHandler = () =>
|
||||
{
|
||||
if (!Capturing) return;
|
||||
if (!UnityEditorInternal.InternalEditorUtility.isApplicationActive) return;
|
||||
|
||||
var customCameraValue = (Camera) _customCamera.value;
|
||||
if (customCameraValue != null && _useCustomCamera.value)
|
||||
{
|
||||
if (_useCustomCamera.value)
|
||||
{
|
||||
customCameraValue.targetTexture = _targetTexture;
|
||||
_captureCamera.targetTexture = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
customCameraValue.targetTexture = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_captureCamera.targetTexture = _targetTexture;
|
||||
var copyFrom = SceneView.lastActiveSceneView.camera;
|
||||
_captureCamera.transform.SetPositionAndRotation(copyFrom.transform.position,
|
||||
copyFrom.transform.rotation);
|
||||
}
|
||||
|
||||
if (!Capturing) return;
|
||||
if (_targetTexture == null) return;
|
||||
|
||||
var rect = EditorGUILayout.GetControlRect();
|
||||
var aspect = _targetTexture.width / (float) _targetTexture.height;
|
||||
var previewRect = new Rect(rect.x, rect.y, rect.width, rect.width / aspect);
|
||||
GUI.DrawTexture(previewRect, _targetTexture);
|
||||
// Enforce repaint immediately
|
||||
_previewContainer.MarkDirtyRepaint();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5baa3e07caa4eeba545fef8f0bc4868
|
||||
timeCreated: 1731031510
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cd5c711d0af4ac382b9c41807cb9d29
|
||||
timeCreated: 1691164551
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83beb30e3257492bbdede1c2964ac250
|
||||
timeCreated: 1691165535
|
||||
@ -0,0 +1,27 @@
|
||||
<UXML xmlns="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns:vrc="VRC.SDKBase.Editor.Elements" xmlns:vrca="VRC.SDK3A.Editor.Elements">
|
||||
<Foldout text="Thumbnail" class="section-foldout" name="thumbnail-foldout">
|
||||
<VisualElement class="row mt-2 mb-2 align-items-start w-100">
|
||||
<vrc:Thumbnail width="192" height="144" hover-text="Image Size 1200 x 900" name="content-thumbnail" />
|
||||
<VisualElement class="col ml-2 flex-grow-1">
|
||||
<VisualElement class="col mb-2" name="thumbnail-selector-block">
|
||||
<Label class="mb-2 white-space-normal m-unity-field " text="You can pick a new image to be used as a thumbnail" />
|
||||
<Button class="mb-2" text="Select New Thumbnail" name="select-new-thumbnail-btn" />
|
||||
<Label class="mb-2 white-space-normal m-unity-field " text="You can also capture the new thumbnail directly from the scene" />
|
||||
<Button text="Capture From Scene" name="capture-thumbnail-from-scene-btn" />
|
||||
</VisualElement>
|
||||
<VisualElement class="col d-none" name="thumbnail-capture-block">
|
||||
<Label class="mb-2 white-space-normal m-unity-field " text="Move your scene view to capture the desired thumbnail" />
|
||||
<Toggle label="Fill Background" tooltip="Uses flat color as a background instead of the skybox" name="thumbnail-fill-background-toggle" />
|
||||
<uie:ColorField label="Background Color" name="thumbnail-background-color-field" class="d-none" />
|
||||
<Toggle label="Use PostProcessing" tooltip="Applies scene post processing to the thumbnail" name="thumbnail-use-post-processing-toggle" />
|
||||
<Toggle label="Use Custom Camera" tooltip="Utilizes a custom camera instead of a VRChat-created camera instead" name="thumbnail-use-custom-camera-toggle" />
|
||||
<uie:ObjectField class="flex-grow-1 d-none" label="Camera" name="thumbnail-custom-camera-ref"/>
|
||||
<VisualElement class="mt-2 row align-items-center" name="thumbnail-capture-confirm-block">
|
||||
<Button class="mr-2 flex-grow-1" text="Capture" name="thumbnail-capture-confirm-btn" />
|
||||
<Button class="flex-grow-1" text="Cancel" name="thumbnail-capture-cancel-btn" />
|
||||
</VisualElement>
|
||||
</VisualElement>
|
||||
</VisualElement>
|
||||
</VisualElement>
|
||||
</Foldout>
|
||||
</UXML>
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a0a0d51d65e4ac9a9ea3861a089855c
|
||||
timeCreated: 1691165551
|
||||
@ -0,0 +1,242 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.UIElements;
|
||||
using Object = UnityEngine.Object;
|
||||
#if POST_PROCESSING_INCLUDED
|
||||
using UnityEngine.Rendering.PostProcessing;
|
||||
#endif
|
||||
|
||||
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
public class ThumbnailFoldout: VisualElement
|
||||
{
|
||||
public new class UxmlFactory : UxmlFactory<ThumbnailFoldout, UxmlTraits> {}
|
||||
public new class UxmlTraits : VisualElement.UxmlTraits
|
||||
{
|
||||
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
|
||||
{
|
||||
get { yield break; }
|
||||
}
|
||||
}
|
||||
|
||||
public Thumbnail Thumbnail { get; private set; }
|
||||
public Foldout Foldout { get; private set; }
|
||||
public EventHandler<string> OnNewThumbnailSelected { get; set; }
|
||||
|
||||
private VisualElement _selectorBlock;
|
||||
private Button _selectThumbnailButton;
|
||||
private Button _captureThumbnailButton;
|
||||
|
||||
private VisualElement _captureBlock;
|
||||
private Toggle _fillBackground;
|
||||
private ColorField _backgroundColor;
|
||||
private Toggle _usePostProcessing;
|
||||
private Toggle _useCustomCamera;
|
||||
private ObjectField _customCamera;
|
||||
private VisualElement _captureConfirmBlock;
|
||||
private Button _captureConfirmButton;
|
||||
private Button _captureCancelButton;
|
||||
|
||||
private string _oldThumbnail;
|
||||
private Texture2D _oldThumbnailTexture;
|
||||
private Camera _captureCamera;
|
||||
private Texture2D _bufferTexture;
|
||||
private RenderTexture _targetTexture;
|
||||
|
||||
private bool _capturing;
|
||||
private bool Capturing
|
||||
{
|
||||
get => _capturing;
|
||||
set
|
||||
{
|
||||
_capturing = value;
|
||||
_selectorBlock.EnableInClassList("d-none", value);
|
||||
_captureBlock.EnableInClassList("d-none", !value);
|
||||
if (value)
|
||||
{
|
||||
_targetTexture = Resources.Load<RenderTexture>("ThumbnailCapture");
|
||||
_captureCamera =
|
||||
VRC_EditorTools.CreateThumbnailCaptureCamera(_targetTexture, _fillBackground.value, _backgroundColor.value, _usePostProcessing.value);
|
||||
_captureCamera.enabled = true;
|
||||
return;
|
||||
}
|
||||
if (_captureCamera != null)
|
||||
{
|
||||
Object.DestroyImmediate(_captureCamera.gameObject);
|
||||
_captureCamera = null;
|
||||
}
|
||||
|
||||
if (_customCamera.value != null)
|
||||
{
|
||||
((Camera) _customCamera.value).targetTexture = null;
|
||||
}
|
||||
|
||||
if (_bufferTexture != null)
|
||||
{
|
||||
Object.DestroyImmediate(_bufferTexture);
|
||||
_bufferTexture = null;
|
||||
}
|
||||
|
||||
_targetTexture = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async void HandleCaptureCancel()
|
||||
{
|
||||
Capturing = false;
|
||||
Thumbnail.ClearImage();
|
||||
if (!string.IsNullOrWhiteSpace(_oldThumbnail))
|
||||
{
|
||||
await Thumbnail.SetImageUrl(_oldThumbnail);
|
||||
}
|
||||
|
||||
if (_oldThumbnailTexture != null)
|
||||
{
|
||||
Thumbnail.SetImage(_oldThumbnailTexture);
|
||||
}
|
||||
|
||||
_oldThumbnail = null;
|
||||
_oldThumbnailTexture = null;
|
||||
}
|
||||
|
||||
public ThumbnailFoldout()
|
||||
{
|
||||
Resources.Load<VisualTreeAsset>("ThumbnailFoldout").CloneTree(this);
|
||||
|
||||
Foldout = this.Q<Foldout>("thumbnail-foldout");
|
||||
Thumbnail = this.Q<Thumbnail>("content-thumbnail");
|
||||
|
||||
_selectorBlock = this.Q<VisualElement>("thumbnail-selector-block");
|
||||
_selectThumbnailButton = this.Q<Button>("select-new-thumbnail-btn");
|
||||
_captureThumbnailButton = this.Q<Button>("capture-thumbnail-from-scene-btn");
|
||||
|
||||
_captureBlock = this.Q<VisualElement>("thumbnail-capture-block");
|
||||
_fillBackground = this.Q<Toggle>("thumbnail-fill-background-toggle");
|
||||
_backgroundColor = this.Q<ColorField>("thumbnail-background-color-field");
|
||||
_usePostProcessing = this.Q<Toggle>("thumbnail-use-post-processing-toggle");
|
||||
_useCustomCamera = this.Q<Toggle>("thumbnail-use-custom-camera-toggle");
|
||||
_customCamera = this.Q<ObjectField>("thumbnail-custom-camera-ref");
|
||||
_captureConfirmBlock = this.Q<VisualElement>("thumbnail-capture-confirm-block");
|
||||
_captureConfirmButton = this.Q<Button>("thumbnail-capture-confirm-btn");
|
||||
_captureCancelButton = this.Q<Button>("thumbnail-capture-cancel-btn");
|
||||
|
||||
#if POST_PROCESSING_INCLUDED
|
||||
_usePostProcessing.RemoveFromClassList("d-none");
|
||||
#endif
|
||||
|
||||
_selectThumbnailButton.clicked += () =>
|
||||
{
|
||||
var imagePath = EditorUtility.OpenFilePanel("Select thumbnail", "", "png");
|
||||
if (string.IsNullOrWhiteSpace(imagePath)) return;
|
||||
OnNewThumbnailSelected?.Invoke(this, imagePath);
|
||||
};
|
||||
|
||||
_captureThumbnailButton.clicked += () =>
|
||||
{
|
||||
_oldThumbnail = Thumbnail.CurrentImage;
|
||||
_oldThumbnailTexture = Thumbnail.CurrentImageTexture;
|
||||
Capturing = true;
|
||||
};
|
||||
|
||||
_captureConfirmButton.clicked += () =>
|
||||
{
|
||||
Capturing = false;
|
||||
var capturedPicture = VRC_EditorTools.CaptureSceneImage(1200, 900, _fillBackground.value,
|
||||
_backgroundColor.value, _usePostProcessing.value, _useCustomCamera.value ? (Camera) _customCamera.value : null);
|
||||
OnNewThumbnailSelected?.Invoke(this, capturedPicture);
|
||||
};
|
||||
|
||||
_captureCancelButton.clicked += HandleCaptureCancel;
|
||||
|
||||
_fillBackground.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (!Capturing || _captureCamera == null) return;
|
||||
_backgroundColor.EnableInClassList("d-none", !evt.newValue);
|
||||
if (evt.newValue)
|
||||
{
|
||||
_captureCamera.clearFlags = CameraClearFlags.SolidColor;
|
||||
_captureCamera.backgroundColor = _backgroundColor.value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_captureCamera.clearFlags = CameraClearFlags.Skybox;
|
||||
}
|
||||
});
|
||||
|
||||
_backgroundColor.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (!Capturing || _captureCamera == null) return;
|
||||
// Enforce alpha to 1
|
||||
_backgroundColor.SetValueWithoutNotify(new Color(evt.newValue.r, evt.newValue.g, evt.newValue.b, 1f));
|
||||
_captureCamera.backgroundColor = _backgroundColor.value;
|
||||
});
|
||||
|
||||
_usePostProcessing.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
#if POST_PROCESSING_INCLUDED
|
||||
if (!Capturing || _captureCamera == null) return;
|
||||
if (_captureCamera.TryGetComponent<PostProcessLayer>(out var layer))
|
||||
{
|
||||
layer.enabled = evt.newValue;
|
||||
}
|
||||
else if (evt.newValue)
|
||||
{
|
||||
var postProcessLayer = _captureCamera.gameObject.AddComponent<PostProcessLayer>();
|
||||
postProcessLayer.volumeLayer = int.MaxValue;
|
||||
postProcessLayer.volumeTrigger = _captureCamera.transform;
|
||||
}
|
||||
#endif
|
||||
});
|
||||
|
||||
_useCustomCamera.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (!Capturing) return;
|
||||
_customCamera.EnableInClassList("d-none", !evt.newValue);
|
||||
});
|
||||
|
||||
_customCamera.objectType = typeof(Camera);
|
||||
_customCamera.allowSceneObjects = true;
|
||||
|
||||
schedule.Execute(() =>
|
||||
{
|
||||
if (!Capturing) return;
|
||||
if (!UnityEditorInternal.InternalEditorUtility.isApplicationActive) return;
|
||||
var customCameraValue = (Camera) _customCamera.value;
|
||||
if (customCameraValue != null && _useCustomCamera.value)
|
||||
{
|
||||
if (_useCustomCamera.value)
|
||||
{
|
||||
customCameraValue.targetTexture = _targetTexture;
|
||||
_captureCamera.targetTexture = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
customCameraValue.targetTexture = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_captureCamera.targetTexture = _targetTexture;
|
||||
var copyFrom = SceneView.lastActiveSceneView.camera;
|
||||
_captureCamera.transform.SetPositionAndRotation(copyFrom.transform.position, copyFrom.transform.rotation);
|
||||
}
|
||||
var req = AsyncGPUReadback.Request(_targetTexture);
|
||||
AsyncGPUReadback.WaitAllRequests();
|
||||
if (!Capturing) return;
|
||||
var data = req.GetData<Color32>();
|
||||
if (_bufferTexture == null)
|
||||
{
|
||||
_bufferTexture = new Texture2D(_targetTexture.width, _targetTexture.height, TextureFormat.RGBA32, false, true);
|
||||
}
|
||||
_bufferTexture.SetPixels32(data.ToArray());
|
||||
_bufferTexture.Apply();
|
||||
Thumbnail.SetImage(_bufferTexture);
|
||||
}).Every(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8625fc371c064cb39e1c550073645721
|
||||
timeCreated: 1691168569
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56fc9bafb78a470e839e5958a78b3f92
|
||||
timeCreated: 1687558315
|
||||
@ -0,0 +1,118 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
[assembly: UxmlNamespacePrefix("VRC.SDKBase.Editor.Elements", "vrc")]
|
||||
namespace VRC.SDKBase.Editor.Elements
|
||||
{
|
||||
public class VRCTextField: TextField
|
||||
{
|
||||
public new class UxmlFactory : UxmlFactory<VRCTextField, UxmlTraits> {}
|
||||
|
||||
public new class UxmlTraits : TextField.UxmlTraits
|
||||
{
|
||||
private readonly UxmlStringAttributeDescription _placeholder = new() { name = "placeholder" };
|
||||
private readonly UxmlBoolAttributeDescription _required = new() { name = "required" };
|
||||
private readonly UxmlBoolAttributeDescription _vertical = new() { name="vertical" };
|
||||
|
||||
public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
|
||||
{
|
||||
get { yield break; }
|
||||
}
|
||||
|
||||
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
|
||||
{
|
||||
base.Init(ve, bag, cc);
|
||||
var textField = (VRCTextField) ve;
|
||||
textField._placeholder = _placeholder.GetValueFromBag(bag, cc);
|
||||
textField._required = _required.GetValueFromBag(bag, cc);
|
||||
var vertical = _vertical.GetValueFromBag(bag, cc);
|
||||
if (textField._required)
|
||||
{
|
||||
textField.label += "*";
|
||||
}
|
||||
if (vertical)
|
||||
{
|
||||
textField.AddToClassList("col");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _placeholder;
|
||||
private static readonly string PlaceholderClass = ussClassName + "__placeholder";
|
||||
private bool _required;
|
||||
private bool _loading;
|
||||
private bool _vertical;
|
||||
|
||||
public bool Loading
|
||||
{
|
||||
get => _loading;
|
||||
set
|
||||
{
|
||||
_loading = value;
|
||||
SetEnabled(!_loading);
|
||||
if (_loading)
|
||||
{
|
||||
text = "Loading...";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (text == "Loading...")
|
||||
{
|
||||
text = "";
|
||||
}
|
||||
FocusOut();
|
||||
}
|
||||
EnableInClassList(ussClassName + "__loading", _loading);
|
||||
}
|
||||
}
|
||||
|
||||
public VRCTextField(): base()
|
||||
{
|
||||
RegisterCallback<FocusOutEvent>(evt => FocusOut());
|
||||
RegisterCallback<FocusInEvent>(evt => FocusIn());
|
||||
this.RegisterValueChangedCallback(ValueChanged);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
FocusOut();
|
||||
return;
|
||||
};
|
||||
RemoveFromClassList(PlaceholderClass);
|
||||
}
|
||||
|
||||
private void ValueChanged(ChangeEvent<string> evt)
|
||||
{
|
||||
if (IsPlaceholder() && !string.IsNullOrEmpty(evt.newValue))
|
||||
{
|
||||
RemoveFromClassList(PlaceholderClass);
|
||||
}
|
||||
if (!_required) return;
|
||||
this.Q<TextInputBase>().EnableInClassList("border-red", string.IsNullOrWhiteSpace(evt.newValue));
|
||||
}
|
||||
|
||||
private void FocusOut()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_placeholder)) return;
|
||||
if (!string.IsNullOrEmpty(text)) return;
|
||||
SetValueWithoutNotify(_placeholder);
|
||||
AddToClassList(PlaceholderClass);
|
||||
}
|
||||
|
||||
private void FocusIn()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_placeholder)) return;
|
||||
if (!this.ClassListContains(PlaceholderClass)) return;
|
||||
this.value = string.Empty;
|
||||
this.RemoveFromClassList(PlaceholderClass);
|
||||
}
|
||||
|
||||
public bool IsPlaceholder()
|
||||
{
|
||||
return ClassListContains(PlaceholderClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b592ca25f524408a84f100c8580649c
|
||||
timeCreated: 1687558321
|
||||
Reference in New Issue
Block a user