using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
#if UNITY_2019_1_OR_NEWER
using UnityEngine.UIElements;
#else
using UnityEngine.Experimental.UIElements;
#endif
namespace Unity.Tutorials.Core.Editor
{
using static Localization;
///
/// Manages masking and highlighting.
///
internal static class MaskingManager
{
///
/// Master control for masking and highlighting.
///
public static UserSetting MaskingEnabled =
new UserSetting("IET.MaskingEnabled", Tr("Enable Masking and Highlighting"), true, Tr("Master control for Masking and Highlighting"));
///
/// Delay, in seconds, before the highlight starts pulsating.
///
public static float HighlightAnimationDelay { get; set; }
///
/// Speed of the highligh pulsation.
///
public static float HighlightAnimationSpeed { get; set; }
internal static bool IsMasked(GUIViewProxy view, List rects)
{
rects.Clear();
MaskViewData maskViewData;
if (s_UnmaskedViews.TryGetValue(view, out maskViewData))
{
var rectList = maskViewData.rects;
rects.AddRange(rectList);
return false;
}
return true;
}
internal static bool IsHighlighted(GUIViewProxy view, List rects)
{
rects.Clear();
MaskViewData maskViewData;
if (!s_HighlightedViews.TryGetValue(view, out maskViewData))
return false;
var rectList = maskViewData.rects;
rects.AddRange(rectList);
return true;
}
static GUIViewProxyComparer s_GUIViewProxyComparer = new GUIViewProxyComparer();
static readonly Dictionary s_UnmaskedViews = new Dictionary(s_GUIViewProxyComparer);
static readonly Dictionary s_HighlightedViews = new Dictionary(s_GUIViewProxyComparer);
static readonly List s_Masks = new List();
static readonly List s_Highlighters = new List();
static double s_LastHighlightTime;
static MaskingManager()
{
EditorApplication.update += delegate
{
// do not animate unless enough time has passed since masking was last applied
var t = EditorApplication.timeSinceStartup - s_LastHighlightTime - HighlightAnimationDelay;
if (t < 0d)
return;
var baseBorderWidth = 4.2f;
var borderWidthAmplitude = 2.1f;
var animatedBorderWidth = Mathf.Cos((float)t * HighlightAnimationSpeed) * borderWidthAmplitude + baseBorderWidth;
foreach (var highlighter in s_Highlighters)
{
if (highlighter == null)
continue;
highlighter.style.borderLeftWidth = animatedBorderWidth;
highlighter.style.borderRightWidth = animatedBorderWidth;
highlighter.style.borderTopWidth = animatedBorderWidth;
highlighter.style.borderBottomWidth = animatedBorderWidth;
}
foreach (var view in s_HighlightedViews)
{
if (view.Key.IsValid)
view.Key.Repaint();
}
};
}
///
/// Unmasks all views.
///
public static void Unmask()
{
foreach (var mask in s_Masks)
{
if (mask != null && mask.parent != null)
mask.parent.Remove(mask);
}
s_Masks.Clear();
foreach (var highlighter in s_Highlighters)
{
if (highlighter != null && highlighter.parent != null)
highlighter.parent.Remove(highlighter);
}
s_Highlighters.Clear();
}
static void CopyMaskData(UnmaskedView.MaskData maskData, Dictionary viewsAndResources)
{
viewsAndResources.Clear();
foreach (var unmaskedView in maskData.m_MaskData)
{
if (unmaskedView.Key == null)
continue;
var maskViewData = unmaskedView.Value;
var unmaskedRegions = maskViewData.rects == null ? new List(1) : maskViewData.rects.ToList();
if (unmaskedRegions.Count == 0)
unmaskedRegions.Add(new Rect(0f, 0f, unmaskedView.Key.Position.width, unmaskedView.Key.Position.height));
viewsAndResources[unmaskedView.Key] = new MaskViewData
{
maskType = maskViewData.maskType,
rects = unmaskedRegions,
EditorWindowType = maskViewData.EditorWindowType
};
}
}
///
/// Adds a mask for a view.
///
///
///
static void AddMaskToView(GUIViewProxy view, VisualElement child)
{
// Since 2019.3(?), we must suppress input to the elements behind masks.
// TODO Doesn't suppress everything, e.g. tooltips are shown still.
child.RegisterCallback((e) => e.StopPropagation());
child.RegisterCallback((e) => e.StopPropagation());
child.RegisterCallback((e) => e.StopPropagation());
child.RegisterCallback((e) => e.StopPropagation());
child.RegisterCallback((e) => e.StopPropagation());
child.RegisterCallback((e) => e.StopPropagation());
child.RegisterCallback((e) => e.StopPropagation());
child.RegisterCallback((e) => e.StopPropagation());
child.RegisterCallback((e) => e.StopPropagation());
if (view.IsDockedToEditor())
{
UIElementsHelper.GetVisualTree(view).Add(child);
}
else
{
var viewVisualElement = UIElementsHelper.GetVisualTree(view);
Debug.Assert(
viewVisualElement.Children().Count() == 2
&& viewVisualElement.Children().Count(viewChild => viewChild is IMGUIContainer) == 1,
"Could not find the expected VisualElement structure"
);
foreach (var visualElement in viewVisualElement.Children())
{
if (!(visualElement is IMGUIContainer))
{
visualElement.Add(child);
break;
}
}
}
}
///
/// Applies masking.
///
///
///
///
///
///
///
public static void Mask(
UnmaskedView.MaskData unmaskedViewsAndRegionsMaskData, Color maskColor,
UnmaskedView.MaskData highlightedRegionsMaskData, Color highlightColor, Color blockedInteractionsColor, float highlightThickness
)
{
Unmask();
CopyMaskData(unmaskedViewsAndRegionsMaskData, s_UnmaskedViews);
CopyMaskData(highlightedRegionsMaskData, s_HighlightedViews);
List views = new List();
GUIViewDebuggerHelperProxy.GetViews(views);
foreach (var view in views)
{
if (!view.IsValid)
continue;
MaskViewData maskViewData;
var viewRect = new Rect(0, 0, view.Position.width, view.Position.height);
// mask everything except the unmasked view rects
if (s_UnmaskedViews.TryGetValue(view, out maskViewData))
{
// Beginning from 2021.2 the layout of floating/undocked EditorWindows has changed a bit and now contains
// an offset caused by the tab area which we need to take into account.
EditorWindow parentWindow = null;
if (maskViewData.EditorWindowType != null)
parentWindow = FindOpenEditorWindowInstance(maskViewData.EditorWindowType);
List rects = maskViewData.rects;
var maskedRects = GetNegativeSpaceRects(viewRect, rects);
for (var i = 0; i < maskedRects.Count; ++i)
{
var rect = maskedRects[i];
if (parentWindow != null && !parentWindow.IsDocked())
{
// In theory we could have an X offset also but it seems highgly unlikely.
rect.y -= parentWindow.rootVisualElement.layout.y;
}
var mask = new VisualElement();
mask.style.backgroundColor = maskColor;
mask.SetLayout(rect);
AddMaskToView(view, mask);
s_Masks.Add(mask);
}
if (maskViewData.maskType == MaskType.BlockInteractions)
{
foreach (var rect in rects)
{
var mask = new VisualElement();
mask.style.backgroundColor = blockedInteractionsColor;
mask.SetLayout(rect);
AddMaskToView(view, mask);
s_Masks.Add(mask);
}
}
}
else // mask the whole view
{
var mask = new VisualElement();
mask.style.backgroundColor = maskColor;
mask.SetLayout(viewRect);
AddMaskToView(view, mask);
s_Masks.Add(mask);
}
if (s_HighlightedViews.TryGetValue(view, out maskViewData))
{
var rects = maskViewData.rects;
// unclip highlight to apply as "outer stroke" if it is being applied to some control(s) in the view
var unclip = rects.Count > 1 || rects[0] != viewRect;
var borderRadius = 5.0f;
foreach (var rect in rects)
{
var highlighter = new VisualElement();
#if UNITY_2019_3_OR_NEWER
highlighter.style.borderLeftColor = highlightColor;
highlighter.style.borderRightColor = highlightColor;
highlighter.style.borderTopColor = highlightColor;
highlighter.style.borderBottomColor = highlightColor;
#else
highlighter.style.borderColor = highlightColor;
#endif
highlighter.style.borderLeftWidth = highlightThickness;
highlighter.style.borderRightWidth = highlightThickness;
highlighter.style.borderTopWidth = highlightThickness;
highlighter.style.borderBottomWidth = highlightThickness;
highlighter.style.borderBottomLeftRadius = borderRadius;
highlighter.style.borderTopLeftRadius = borderRadius;
highlighter.style.borderBottomRightRadius = borderRadius;
highlighter.style.borderTopRightRadius = borderRadius;
highlighter.pickingMode = PickingMode.Ignore;
var layout = rect;
if (unclip)
{
layout.xMin -= highlightThickness;
layout.xMax += highlightThickness;
layout.yMin -= highlightThickness;
layout.yMax += highlightThickness;
}
highlighter.SetLayout(layout);
UIElementsHelper.GetVisualTree(view).Add(highlighter);
s_Highlighters.Add(highlighter);
}
}
}
s_LastHighlightTime = EditorApplication.timeSinceStartup;
}
static EditorWindow FindOpenEditorWindowInstance(System.Type type) =>
Resources.FindObjectsOfTypeAll(type).FirstOrDefault() as EditorWindow;
static readonly HashSet s_YCoords = new HashSet();
static readonly HashSet s_XCoords = new HashSet();
static readonly List s_SortedYCoords = new List();
static readonly List s_SortedXCoords = new List();
internal static List GetNegativeSpaceRects(Rect viewRect, List positiveSpaceRects)
{
//TODO maybe its okay to round to int?
s_YCoords.Clear();
s_XCoords.Clear();
for (int i = 0; i < positiveSpaceRects.Count; i++)
{
var hole = positiveSpaceRects[i];
s_YCoords.Add(hole.y);
s_YCoords.Add(hole.yMax);
s_XCoords.Add(hole.x);
s_XCoords.Add(hole.xMax);
}
s_YCoords.Add(0);
s_YCoords.Add(viewRect.height);
s_XCoords.Add(0);
s_XCoords.Add(viewRect.width);
s_SortedYCoords.Clear();
s_SortedXCoords.Clear();
s_SortedYCoords.AddRange(s_YCoords);
s_SortedXCoords.AddRange(s_XCoords);
s_SortedYCoords.Sort();
s_SortedXCoords.Sort();
var filledRects = new List();
for (var i = 1; i < s_SortedYCoords.Count; ++i)
{
var minY = s_SortedYCoords[i - 1];
var maxY = s_SortedYCoords[i];
var midY = (maxY + minY) / 2;
var workingRect = new Rect(s_SortedXCoords[0], minY, 0, (maxY - minY));
for (var j = 1; j < s_SortedXCoords.Count; ++j)
{
var minX = s_SortedXCoords[j - 1];
var maxX = s_SortedXCoords[j];
var midX = (maxX + minX) / 2;
var potentialHole = positiveSpaceRects.Find((hole) => { return hole.Contains(new Vector2(midX, midY)); });
var cellIsHole = potentialHole.width > 0 && potentialHole.height > 0;
if (cellIsHole)
{
if (workingRect.width > 0 && workingRect.height > 0)
filledRects.Add(workingRect);
workingRect.x = maxX;
workingRect.xMax = maxX;
}
else
{
workingRect.xMax = maxX;
}
}
if (workingRect.width > 0 && workingRect.height > 0)
filledRects.Add(workingRect);
}
return filledRects;
}
}
}