Files
osu-framework/osu.Framework/Graphics/Visualisation/VisualisedDrawable.cs
Joseph Madamba 83c7bc63b8 Truncate drawable class names so that position and size are always visible
Also adds a tooltip of the full class name when truncated.
2025-09-07 11:26:23 -07:00

526 lines
18 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
namespace osu.Framework.Graphics.Visualisation
{
internal partial class VisualisedDrawable : Container, IContainVisualisedDrawables, IFilterable
{
private const int line_height = 12;
private const float text_x_pos = 30;
public Drawable Target { get; }
private bool isHighlighted;
public bool IsHighlighted
{
get => isHighlighted;
set
{
isHighlighted = value;
updateColours();
if (value)
Expand();
}
}
public IEnumerable<LocalisableString> FilterTerms => new LocalisableString[]
{
Target.ToString()
};
public bool FilteringActive { get; set; }
private bool matchingFilter = true;
public bool MatchingFilter
{
get => matchingFilter;
set
{
bool wasPresent = IsPresent;
matchingFilter = value;
if (IsPresent != wasPresent)
Invalidate(Invalidation.Presence);
}
}
public override bool IsPresent => base.IsPresent && MatchingFilter;
public Action<Drawable> RequestTarget;
public Action<VisualisedDrawable> HighlightTarget;
private Box background;
private TooltipSpriteText text;
private SpriteText text2;
private Drawable previewBox;
private Drawable activityInvalidate;
private Drawable activityAutosize;
private Drawable activityLayout;
private Drawable inputReceiving;
private VisualisedDrawableFlow flow;
private Container connectionContainer;
private const float row_width = 10;
private const float row_height = 20;
[Resolved]
private DrawVisualiser visualiser { get; set; }
[Resolved]
private TreeContainer tree { get; set; }
public VisualisedDrawable(Drawable d)
{
Target = d;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
var spriteTarget = Target as Sprite;
AddRange(new Drawable[]
{
flow = new VisualisedDrawableFlow
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Position = new Vector2(0, row_height),
Padding = new MarginPadding { Left = row_width },
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(100, 1), // a bit of a hack, but works well enough.
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.Transparent,
},
inputReceiving = new Container
{
Size = new Vector2(5, line_height),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Position = new Vector2(9, 0),
Alpha = 0,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Cyan,
RelativeSizeAxes = Axes.Both,
},
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.Cyan.Darken(5),
Icon = FontAwesome.Solid.MousePointer,
Size = new Vector2(6),
}
}
},
activityInvalidate = new Box
{
Colour = Color4.Yellow,
Size = new Vector2(2, line_height),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Position = new Vector2(6, 0),
Alpha = 0
},
activityLayout = new Box
{
Colour = Color4.Orange,
Size = new Vector2(2, line_height),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Position = new Vector2(3, 0),
Alpha = 0
},
activityAutosize = new Box
{
Colour = Color4.Red,
Size = new Vector2(2, line_height),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Position = new Vector2(0, 0),
Alpha = 0
},
previewBox = spriteTarget?.Texture == null
? previewBox = new Box
{
Colour = Color4.White,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}
: new Sprite
{
// It's fine to only bypass the ref count, because this sprite will dispose along with the original sprite
Texture = new Texture(spriteTarget.Texture),
Scale = new Vector2(spriteTarget.Texture.DisplayWidth / spriteTarget.Texture.DisplayHeight, 1),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Position = new Vector2(text_x_pos, 0),
Children = new Drawable[]
{
text = new TooltipSpriteText { Font = FrameworkFont.Regular },
text2 = new SpriteText { Font = FrameworkFont.Regular },
}
},
}
},
});
const float connection_width = 1;
AddInternal(connectionContainer = new Container
{
Colour = FrameworkColour.Green,
RelativeSizeAxes = Axes.Y,
Width = connection_width,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
EdgeSmoothness = new Vector2(0.5f),
},
new Box
{
Anchor = Anchor.TopRight,
Origin = Anchor.CentreLeft,
Y = row_height / 2,
Width = row_width / 2,
EdgeSmoothness = new Vector2(0.5f),
}
}
});
previewBox.Position = new Vector2(15, 0);
previewBox.Size = new Vector2(line_height, line_height);
var compositeTarget = Target as CompositeDrawable;
compositeTarget?.AliveInternalChildren.ForEach(addChild);
updateSpecifics();
}
protected override void LoadComplete()
{
base.LoadComplete();
attachEvents();
updateColours();
}
public bool TopLevel
{
set => connectionContainer.Alpha = value ? 0 : 1;
}
private void attachEvents()
{
Target.Invalidated += onInvalidated;
Target.OnDispose += onDispose;
if (Target is CompositeDrawable da)
{
da.OnAutoSize += onAutoSize;
da.ChildBecameAlive += addChild;
da.ChildDied += removeChild;
da.ChildDepthChanged += depthChanged;
}
if (Target is FlowContainer<Drawable> df) df.OnLayout += onLayout;
}
private void detachEvents()
{
Target.Invalidated -= onInvalidated;
Target.OnDispose -= onDispose;
if (Target is CompositeDrawable da)
{
da.OnAutoSize -= onAutoSize;
da.ChildBecameAlive -= addChild;
da.ChildDied -= removeChild;
da.ChildDepthChanged -= depthChanged;
}
if (Target is FlowContainer<Drawable> df) df.OnLayout -= onLayout;
}
private void addChild(Drawable drawable)
{
// Make sure to never add the DrawVisualiser (recursive scenario)
if (drawable == visualiser) return;
// Don't add individual characters of SpriteText
if (Target is SpriteText) return;
visualiser.GetVisualiserFor(drawable).SetContainer(this);
}
private void removeChild(Drawable drawable)
{
var vis = visualiser.GetVisualiserFor(drawable);
if (vis.currentContainer == this)
vis.SetContainer(null);
}
private void depthChanged(Drawable drawable)
{
var vis = visualiser.GetVisualiserFor(drawable);
vis.currentContainer?.RemoveVisualiser(vis);
vis.currentContainer?.AddVisualiser(vis);
}
void IContainVisualisedDrawables.AddVisualiser(VisualisedDrawable visualiser)
{
visualiser.RequestTarget = d => RequestTarget?.Invoke(d);
visualiser.HighlightTarget = d => HighlightTarget?.Invoke(d);
visualiser.Depth = visualiser.Target.Depth;
flow.Add(visualiser);
}
void IContainVisualisedDrawables.RemoveVisualiser(VisualisedDrawable visualiser) => flow.Remove(visualiser, false);
public VisualisedDrawable FindVisualisedDrawable(Drawable drawable)
{
if (drawable == Target)
return this;
foreach (var child in flow)
{
var vis = child.FindVisualisedDrawable(drawable);
if (vis != null)
return vis;
}
return null;
}
protected override void Dispose(bool isDisposing)
{
detachEvents();
base.Dispose(isDisposing);
}
protected override bool OnHover(HoverEvent e)
{
updateColours();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateColours();
base.OnHoverLost(e);
}
private void updateColours()
{
if (isHighlighted)
{
background.Colour = FrameworkColour.YellowGreen;
text.Colour = FrameworkColour.Blue;
text2.Colour = FrameworkColour.Blue;
}
else if (IsHovered)
{
background.Colour = FrameworkColour.BlueGreen;
text.Colour = Color4.White;
text2.Colour = FrameworkColour.YellowGreen;
}
else
{
background.Colour = Color4.Transparent;
text.Colour = Color4.White;
text2.Colour = FrameworkColour.YellowGreen;
}
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Right)
{
HighlightTarget?.Invoke(this);
return true;
}
return false;
}
protected override bool OnClick(ClickEvent e)
{
if (isExpanded)
Collapse();
else
Expand();
return true;
}
protected override bool OnDoubleClick(DoubleClickEvent e)
{
RequestTarget?.Invoke(Target);
return true;
}
private bool isExpanded = true;
public void Expand()
{
flow.FadeIn();
updateSpecifics();
isExpanded = true;
}
public void ExpandAll()
{
Expand();
flow.ForEach(f => f.Expand());
}
public void Collapse()
{
flow.FadeOut();
updateSpecifics();
isExpanded = false;
}
private void onAutoSize() => activityAutosize.FadeOutFromOne(1);
private void onLayout() => activityLayout.FadeOutFromOne(1);
private void onInvalidated(Drawable d, Invalidation invalidation) => activityInvalidate.FadeOutFromOne(1);
private void onDispose()
{
// May come from the disposal thread, in which case they won't ever be reused and the container doesn't need to be reset
Schedule(() => SetContainer(null));
}
private void updateSpecifics()
{
Vector2 posInTree = ToSpaceOfOtherDrawable(Vector2.Zero, tree);
inputReceiving.Alpha =
Target.GetContainingInputManager() is InputManager inputManager &&
Target.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position)
? 1
: 0;
if (posInTree.Y < -previewBox.DrawHeight || posInTree.Y > tree.Height)
{
text.Text = string.Empty;
return;
}
previewBox.Alpha = Math.Max(0.2f, Target.Alpha);
previewBox.Colour = Target.Colour;
int childCount = (Target as CompositeDrawable)?.InternalChildren.Count ?? 0;
text.Text = Target.ToString();
text2.Text = $" ({Target.DrawPosition.X:#,0},{Target.DrawPosition.Y:#,0}) {Target.DrawSize.X:#,0}x{Target.DrawSize.Y:#,0}"
+ (!isExpanded && childCount > 0 ? $@" ({childCount} children)" : string.Empty);
Alpha = Target.IsPresent ? 1 : 0.3f;
}
protected override void Update()
{
updateSpecifics();
text.MaxWidth = DrawWidth - text_x_pos - text2.DrawWidth - 5;
base.Update();
}
private IContainVisualisedDrawables currentContainer;
/// <summary>
/// Moves this <see cref="VisualisedDrawable"/> to be contained by another target.
/// </summary>
/// <remarks>
/// The <see cref="VisualisedDrawable"/> is first removed from its current container via <see cref="IContainVisualisedDrawables.RemoveVisualiser"/>,
/// prior to being added to the new container via <see cref="IContainVisualisedDrawables.AddVisualiser"/>.
/// </remarks>
/// <param name="container">The target which should contain this <see cref="VisualisedDrawable"/>.</param>
public void SetContainer(IContainVisualisedDrawables container)
{
currentContainer?.RemoveVisualiser(this);
// The visualised may have previously been within a container (e.g. flow), which repositioned it
// We should make sure that the position is reset before it's added to another container
Y = 0;
container?.AddVisualiser(this);
currentContainer = container;
}
private partial class VisualisedDrawableFlow : FillFlowContainer<VisualisedDrawable>
{
public override IEnumerable<Drawable> FlowingChildren => AliveInternalChildren.Where(d => d.IsPresent).OrderBy(d => -d.Depth).ThenBy(d => ((VisualisedDrawable)d).Target.ChildID);
}
private partial class TooltipSpriteText : SpriteText, IHasTooltip
{
public LocalisableString TooltipText => Text;
public override bool HandlePositionalInput => IsTruncated;
public TooltipSpriteText()
{
Truncate = true;
}
}
}
}