using System;
using System.Collections.Generic;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace UnityEditor.Rendering.LookDev
{
/// Interface that must implement the EnvironmentLibrary view to communicate with the data management
public interface IEnvironmentDisplayer
{
/// Repaint the UI
void Repaint();
/// Callback on Environment change in the Library
event Action OnChangingEnvironmentLibrary;
}
partial class DisplayWindow : IEnvironmentDisplayer
{
static partial class Style
{
internal static readonly Texture2D k_AddIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Add", forceLowRes: true);
internal static readonly Texture2D k_RemoveIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Remove", forceLowRes: true);
internal static readonly Texture2D k_DuplicateIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Duplicate", forceLowRes: true);
internal const string k_DragAndDropLibrary = "Drag and drop EnvironmentLibrary here";
}
VisualElement m_EnvironmentContainer;
ListView m_EnvironmentList;
EnvironmentElement m_EnvironmentInspector;
UIElements.Toolbar m_EnvironmentListToolbar;
UIElements.ObjectField m_LibraryField;
//event Action OnAddingEnvironmentInternal;
//event Action IEnvironmentDisplayer.OnAddingEnvironment
//{
// add => OnAddingEnvironmentInternal += value;
// remove => OnAddingEnvironmentInternal -= value;
//}
//event Action OnRemovingEnvironmentInternal;
//event Action IEnvironmentDisplayer.OnRemovingEnvironment
//{
// add => OnRemovingEnvironmentInternal += value;
// remove => OnRemovingEnvironmentInternal -= value;
//}
event Action OnChangingEnvironmentLibraryInternal;
event Action IEnvironmentDisplayer.OnChangingEnvironmentLibrary
{
add => OnChangingEnvironmentLibraryInternal += value;
remove => OnChangingEnvironmentLibraryInternal -= value;
}
static int FirstVisibleIndex(ListView listView)
=> (int)(listView.Q().scrollOffset.y / listView.itemHeight);
void CreateEnvironment()
{
if (m_MainContainer == null || m_MainContainer.Equals(null))
throw new System.MemberAccessException("m_MainContainer should be assigned prior CreateEnvironment()");
m_EnvironmentContainer = new VisualElement() { name = Style.k_EnvironmentContainerName };
m_MainContainer.Add(m_EnvironmentContainer);
if (sidePanel == SidePanel.Environment)
m_MainContainer.AddToClassList(Style.k_ShowEnvironmentPanelClass);
m_EnvironmentInspector = new EnvironmentElement(withPreview: false, () =>
{
LookDev.SaveContextChangeAndApply(ViewIndex.First);
LookDev.SaveContextChangeAndApply(ViewIndex.Second);
});
m_EnvironmentList = new ListView();
m_EnvironmentList.AddToClassList("list-environment");
m_EnvironmentList.selectionType = SelectionType.Single;
m_EnvironmentList.itemHeight = EnvironmentElement.k_SkyThumbnailHeight;
m_EnvironmentList.makeItem = () =>
{
var preview = new Image();
preview.AddManipulator(new EnvironmentPreviewDragger(this, m_ViewContainer));
return preview;
};
m_EnvironmentList.bindItem = (e, i) =>
{
if (LookDev.currentContext.environmentLibrary == null)
return;
(e as Image).image = EnvironmentElement.GetLatLongThumbnailTexture(
LookDev.currentContext.environmentLibrary[i],
EnvironmentElement.k_SkyThumbnailWidth);
};
#if UNITY_2020_1_OR_NEWER
m_EnvironmentList.onSelectionChange += objects =>
{
bool empty = !objects.GetEnumerator().MoveNext();
if (empty || (LookDev.currentContext.environmentLibrary?.Count ?? 0) == 0)
#else
m_EnvironmentList.onSelectionChanged += objects =>
{
if (objects.Count == 0 || (LookDev.currentContext.environmentLibrary?.Count ?? 0) == 0)
#endif
{
m_EnvironmentInspector.style.visibility = Visibility.Hidden;
m_EnvironmentInspector.style.height = 0;
}
else
{
m_EnvironmentInspector.style.visibility = Visibility.Visible;
m_EnvironmentInspector.style.height = new StyleLength(StyleKeyword.Auto);
int firstVisibleIndex = FirstVisibleIndex(m_EnvironmentList);
Environment environment = LookDev.currentContext.environmentLibrary[m_EnvironmentList.selectedIndex];
var container = m_EnvironmentList.Q("unity-content-container");
if (m_EnvironmentList.selectedIndex - firstVisibleIndex >= container.childCount || m_EnvironmentList.selectedIndex < firstVisibleIndex)
{
m_EnvironmentList.ScrollToItem(m_EnvironmentList.selectedIndex);
firstVisibleIndex = FirstVisibleIndex(m_EnvironmentList);
}
Image deportedLatLong = container[m_EnvironmentList.selectedIndex - firstVisibleIndex] as Image;
m_EnvironmentInspector.Bind(environment, deportedLatLong);
}
};
#if UNITY_2020_1_OR_NEWER
m_EnvironmentList.onItemsChosen += objCollection =>
{
foreach(var obj in objCollection)
EditorGUIUtility.PingObject(LookDev.currentContext.environmentLibrary?[(int)obj]);
};
#else
m_EnvironmentList.onItemChosen += obj =>
EditorGUIUtility.PingObject(LookDev.currentContext.environmentLibrary?[(int)obj]);
#endif
m_NoEnvironmentList = new Label(Style.k_DragAndDropLibrary);
m_NoEnvironmentList.style.flexGrow = 1;
m_NoEnvironmentList.style.unityTextAlign = TextAnchor.MiddleCenter;
m_EnvironmentContainer.Add(m_EnvironmentInspector);
m_EnvironmentListToolbar = new UIElements.Toolbar();
ToolbarButton addEnvironment = new ToolbarButton(() =>
{
if (LookDev.currentContext.environmentLibrary == null)
return;
LookDev.currentContext.environmentLibrary.Add();
RefreshLibraryDisplay();
m_EnvironmentList.ScrollToItem(-1); //-1: scroll to end
m_EnvironmentList.selectedIndex = LookDev.currentContext.environmentLibrary.Count - 1;
ScrollToEnd();
})
{
name = "add",
tooltip = "Add new empty environment"
};
addEnvironment.Add(new Image() { image = Style.k_AddIcon });
ToolbarButton removeEnvironment = new ToolbarButton(() =>
{
if (m_EnvironmentList.selectedIndex == -1 || LookDev.currentContext.environmentLibrary == null)
return;
LookDev.currentContext.environmentLibrary?.Remove(m_EnvironmentList.selectedIndex);
RefreshLibraryDisplay();
m_EnvironmentList.selectedIndex = -1;
})
{
name = "remove",
tooltip = "Remove environment currently selected"
};
removeEnvironment.Add(new Image() { image = Style.k_RemoveIcon });
ToolbarButton duplicateEnvironment = new ToolbarButton(() =>
{
if (m_EnvironmentList.selectedIndex == -1 || LookDev.currentContext.environmentLibrary == null)
return;
LookDev.currentContext.environmentLibrary.Duplicate(m_EnvironmentList.selectedIndex);
RefreshLibraryDisplay();
m_EnvironmentList.ScrollToItem(-1); //-1: scroll to end
m_EnvironmentList.selectedIndex = LookDev.currentContext.environmentLibrary.Count - 1;
ScrollToEnd();
})
{
name = "duplicate",
tooltip = "Duplicate environment currently selected"
};
duplicateEnvironment.Add(new Image() { image = Style.k_DuplicateIcon });
m_EnvironmentListToolbar.Add(addEnvironment);
m_EnvironmentListToolbar.Add(removeEnvironment);
m_EnvironmentListToolbar.Add(duplicateEnvironment);
m_EnvironmentListToolbar.AddToClassList("list-environment-overlay");
var m_EnvironmentInspectorSeparator = new VisualElement() { name = "separator-line" };
m_EnvironmentInspectorSeparator.Add(new VisualElement() { name = "separator" });
m_EnvironmentContainer.Add(m_EnvironmentInspectorSeparator);
VisualElement listContainer = new VisualElement();
listContainer.AddToClassList("list-environment");
listContainer.Add(m_EnvironmentList);
listContainer.Add(m_EnvironmentListToolbar);
m_LibraryField = new ObjectField("Library")
{
tooltip = "The currently used library"
};
m_LibraryField.allowSceneObjects = false;
m_LibraryField.objectType = typeof(EnvironmentLibrary);
m_LibraryField.SetValueWithoutNotify(LookDev.currentContext.environmentLibrary);
m_LibraryField.RegisterValueChangedCallback(evt =>
{
m_EnvironmentList.selectedIndex = -1;
OnChangingEnvironmentLibraryInternal?.Invoke(evt.newValue as EnvironmentLibrary);
RefreshLibraryDisplay();
});
var environmentListCreationToolbar = new UIElements.Toolbar()
{
name = "environmentListCreationToolbar"
};
environmentListCreationToolbar.Add(m_LibraryField);
environmentListCreationToolbar.Add(new ToolbarButton(()
=> EnvironmentLibraryCreator.CreateAndAssignTo(m_LibraryField))
{
text = "New",
tooltip = "Create a new EnvironmentLibrary"
});
m_EnvironmentContainer.Add(listContainer);
m_EnvironmentContainer.Add(m_NoEnvironmentList);
m_EnvironmentContainer.Add(environmentListCreationToolbar);
//add ability to unselect
m_EnvironmentList.RegisterCallback(evt =>
{
var clickedIndex = (int)(evt.localMousePosition.y / m_EnvironmentList.itemHeight);
if (clickedIndex >= m_EnvironmentList.itemsSource.Count)
{
m_EnvironmentList.selectedIndex = -1;
evt.StopPropagation();
}
});
RefreshLibraryDisplay();
}
//necessary as the scrollview need to be updated which take some editor frames.
void ScrollToEnd(int attemptRemaining = 5)
{
m_EnvironmentList.ScrollToItem(-1); //-1: scroll to end
if (attemptRemaining > 0)
EditorApplication.delayCall += () => ScrollToEnd(--attemptRemaining);
}
void RefreshLibraryDisplay()
{
if (m_LibraryField != null)
m_LibraryField.SetValueWithoutNotify(LookDev.currentContext.environmentLibrary);
if (m_EnvironmentInspector != null && m_EnvironmentList != null)
{
int itemMax = LookDev.currentContext.environmentLibrary?.Count ?? 0;
if (itemMax == 0 || m_EnvironmentList.selectedIndex == -1)
{
m_EnvironmentInspector.style.visibility = Visibility.Hidden;
m_EnvironmentInspector.style.height = 0;
}
else
{
m_EnvironmentInspector.style.visibility = Visibility.Visible;
m_EnvironmentInspector.style.height = new StyleLength(StyleKeyword.Auto);
}
var items = new List(itemMax);
for (int i = 0; i < itemMax; i++)
items.Add(i);
m_EnvironmentList.itemsSource = items;
if (LookDev.currentContext.environmentLibrary == null)
{
m_EnvironmentList
.Q(className: "unity-scroll-view__vertical-scroller")
.Q("unity-dragger")
.style.visibility = Visibility.Hidden;
m_EnvironmentListToolbar.style.visibility = Visibility.Hidden;
m_NoEnvironmentList.style.display = DisplayStyle.Flex;
}
else
{
m_EnvironmentList
.Q(className: "unity-scroll-view__vertical-scroller")
.Q("unity-dragger")
.style.visibility = itemMax == 0
? Visibility.Hidden
: Visibility.Visible;
m_EnvironmentListToolbar.style.visibility = Visibility.Visible;
m_NoEnvironmentList.style.display = DisplayStyle.None;
}
}
}
DraggingContext StartDragging(VisualElement item, Vector2 worldPosition)
=> new DraggingContext(
rootVisualElement,
item as Image,
//note: this even can come before the selection event of the
//ListView. Reconstruct index by looking at target of the event.
(int)item.layout.y / m_EnvironmentList.itemHeight,
worldPosition);
void EndDragging(DraggingContext context, Vector2 mouseWorldPosition)
{
Environment environment = LookDev.currentContext.environmentLibrary?[context.draggedIndex];
if (environment == null)
return;
if (m_Views[(int)ViewIndex.First].ContainsPoint(mouseWorldPosition))
{
if (viewLayout == Layout.CustomSplit)
OnChangingEnvironmentInViewInternal?.Invoke(environment, ViewCompositionIndex.Composite, mouseWorldPosition);
else
OnChangingEnvironmentInViewInternal?.Invoke(environment, ViewCompositionIndex.First, mouseWorldPosition);
m_NoEnvironment1.style.visibility = environment == null || environment.Equals(null) ? Visibility.Visible : Visibility.Hidden;
}
else
{
OnChangingEnvironmentInViewInternal?.Invoke(environment, ViewCompositionIndex.Second, mouseWorldPosition);
m_NoEnvironment2.style.visibility = environment == null || environment.Equals(null) ? Visibility.Visible : Visibility.Hidden;
}
}
class DraggingContext : IDisposable
{
const string k_CursorFollowerName = "cursorFollower";
public readonly int draggedIndex;
readonly Image cursorFollower;
readonly Vector2 cursorOffset;
readonly VisualElement windowContent;
public DraggingContext(VisualElement windowContent, Image draggedElement, int draggedIndex, Vector2 worldPosition)
{
this.windowContent = windowContent;
this.draggedIndex = draggedIndex;
cursorFollower = new Image()
{
name = k_CursorFollowerName,
image = draggedElement.image
};
cursorFollower.tintColor = new Color(1f, 1f, 1f, .5f);
windowContent.Add(cursorFollower);
cursorOffset = draggedElement.WorldToLocal(worldPosition);
cursorFollower.style.position = Position.Absolute;
UpdateCursorFollower(worldPosition);
}
public void UpdateCursorFollower(Vector2 mouseWorldPosition)
{
Vector2 windowLocalPosition = windowContent.WorldToLocal(mouseWorldPosition);
cursorFollower.style.left = windowLocalPosition.x - cursorOffset.x;
cursorFollower.style.top = windowLocalPosition.y - cursorOffset.y;
}
public void Dispose()
{
if (windowContent.Contains(cursorFollower))
windowContent.Remove(cursorFollower);
}
}
class EnvironmentPreviewDragger : Manipulator
{
VisualElement m_DropArea;
DisplayWindow m_Window;
//Note: static as only one drag'n'drop at a time
static DraggingContext s_Context;
public EnvironmentPreviewDragger(DisplayWindow window, VisualElement dropArea)
{
m_Window = window;
m_DropArea = dropArea;
}
protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback(OnMouseDown);
target.RegisterCallback(OnMouseUp);
}
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback(OnMouseDown);
target.UnregisterCallback(OnMouseUp);
}
void Release()
{
target.UnregisterCallback(OnMouseMove);
s_Context?.Dispose();
target.ReleaseMouse();
s_Context = null;
}
void OnMouseDown(MouseDownEvent evt)
{
if (evt.button == 0)
{
target.CaptureMouse();
target.RegisterCallback(OnMouseMove);
s_Context = m_Window.StartDragging(target, evt.mousePosition);
//do not stop event as we still need to propagate it to the ListView for selection
}
}
void OnMouseUp(MouseUpEvent evt)
{
if (evt.button != 0)
return;
if (m_DropArea.ContainsPoint(m_DropArea.WorldToLocal(Event.current.mousePosition)))
{
m_Window.EndDragging(s_Context, evt.mousePosition);
evt.StopPropagation();
}
Release();
}
void OnMouseMove(MouseMoveEvent evt)
{
evt.StopPropagation();
s_Context.UpdateCursorFollower(evt.mousePosition);
}
}
void IEnvironmentDisplayer.Repaint()
{
//can be unsync if library asset is destroy by user, so if null force sync
if (LookDev.currentContext.environmentLibrary == null)
m_LibraryField.value = null;
RefreshLibraryDisplay();
}
void OnFocus()
{
//OnFocus is called before OnEnable that open backend if not already opened, so only sync if backend is open
if (LookDev.open)
{
//If EnvironmentLibrary asset as been edited by the user (deletion),
//update all view to use null environment if it was not temporary ones
if (LookDev.currentContext.HasLibraryAssetChanged(m_LibraryField.value as EnvironmentLibrary))
{
ViewContext viewContext = LookDev.currentContext.GetViewContent(ViewIndex.First);
if (!(viewContext.environment?.isCubemapOnly ?? false))
OnChangingEnvironmentInViewInternal?.Invoke(viewContext.environment, ViewCompositionIndex.First, default);
viewContext = LookDev.currentContext.GetViewContent(ViewIndex.Second);
if (!(viewContext.environment?.isCubemapOnly ?? false))
OnChangingEnvironmentInViewInternal?.Invoke(viewContext.environment, ViewCompositionIndex.Second, default);
}
//If Cubemap asset as been edited by the user (deletion),
//update all views to use null environment if it was temporary ones
//and update all other views' environment to not use cubemap anymore
foreach (ViewContext viewContext in LookDev.currentContext.viewContexts)
{
if (viewContext.environment == null || !viewContext.environment.HasCubemapAssetChanged(viewContext.environment.cubemap))
continue;
if (viewContext.environment.isCubemapOnly)
viewContext.UpdateEnvironment(null);
else
viewContext.environment.cubemap = null;
}
((IEnvironmentDisplayer)this).Repaint();
}
}
void FullRefreshEnvironmentList()
{
if (LookDev.currentContext.environmentLibrary != null)
LookDev.currentContext.FullReimportEnvironmentLibrary();
((IEnvironmentDisplayer)this).Repaint();
}
}
}