1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 10:07:36 +08:00
osu-lazer/osu.Game/Skinning/Editor/SkinEditor.cs
2022-03-16 19:13:52 +09:00

393 lines
14 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.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.Play;
using osu.Game.Screens.Select;
using osuTK;
namespace osu.Game.Skinning.Editor
{
[Cached(typeof(SkinEditor))]
public class SkinEditor : VisibilityContainer
{
public const double TRANSITION_DURATION = 500;
public readonly BindableList<ISkinnableDrawable> SelectedComponents = new BindableList<ISkinnableDrawable>();
protected override bool StartHidden => true;
private Drawable targetScreen;
private OsuTextFlowContainer headerText;
private Bindable<Skin> currentSkin;
[Resolved]
private SkinManager skins { get; set; }
[Resolved]
private OsuColour colours { get; set; }
private bool hasBegunMutating;
private Container content;
private EditorToolboxGroup settingsToolbox;
public SkinEditor()
{
}
public SkinEditor(Drawable targetScreen)
{
UpdateTargetScreen(targetScreen);
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
const float menu_height = 40;
InternalChild = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Container
{
Name = "Menu container",
RelativeSizeAxes = Axes.X,
Depth = float.MinValue,
Height = menu_height,
Children = new Drawable[]
{
new EditorMenuBar
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Items = new[]
{
new MenuItem("File")
{
Items = new[]
{
new EditorMenuItem("Save", MenuItemType.Standard, Save),
new EditorMenuItem("Revert to default", MenuItemType.Destructive, revert),
new EditorMenuItemSpacer(),
new EditorMenuItem("Exit", MenuItemType.Standard, Hide),
},
},
}
},
headerText = new OsuTextFlowContainer
{
TextAnchor = Anchor.TopRight,
Padding = new MarginPadding(5),
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
},
},
},
new SceneLibrary
{
RelativeSizeAxes = Axes.X,
Y = menu_height,
},
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new SkinComponentToolbox(600)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RequestPlacement = placeComponent
},
content = new Container
{
RelativeSizeAxes = Axes.Both,
},
settingsToolbox = new SkinSettingsToolbox
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
}
}
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Show();
// as long as the skin editor is loaded, let's make sure we can modify the current skin.
currentSkin = skins.CurrentSkin.GetBoundCopy();
// schedule ensures this only happens when the skin editor is visible.
// also avoid some weird endless recursion / bindable feedback loop (something to do with tracking skins across three different bindable types).
// probably something which will be factored out in a future database refactor so not too concerning for now.
currentSkin.BindValueChanged(skin =>
{
hasBegunMutating = false;
Scheduler.AddOnce(skinChanged);
}, true);
SelectedComponents.BindCollectionChanged((_, __) => Scheduler.AddOnce(populateSettings), true);
}
public void UpdateTargetScreen(Drawable targetScreen)
{
this.targetScreen = targetScreen;
SelectedComponents.Clear();
Scheduler.AddOnce(loadBlueprintContainer);
void loadBlueprintContainer()
{
content.Children = new Drawable[]
{
new SkinBlueprintContainer(targetScreen)
{
Margin = new MarginPadding { Top = 100 },
}
};
}
}
private void skinChanged()
{
headerText.Clear();
headerText.AddParagraph("Skin editor", cp => cp.Font = OsuFont.Default.With(size: 16));
headerText.NewParagraph();
headerText.AddText("Currently editing ", cp =>
{
cp.Font = OsuFont.Default.With(size: 12);
cp.Colour = colours.Yellow;
});
headerText.AddText($"{currentSkin.Value.SkinInfo}", cp =>
{
cp.Font = OsuFont.Default.With(size: 12, weight: FontWeight.Bold);
cp.Colour = colours.Yellow;
});
skins.EnsureMutableSkin();
hasBegunMutating = true;
}
private void placeComponent(Type type)
{
var target = availableTargets.FirstOrDefault()?.Target;
if (target == null)
return;
var targetContainer = getTarget(target.Value);
if (targetContainer == null)
return;
if (!(Activator.CreateInstance(type) is ISkinnableDrawable component))
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}.");
var drawableComponent = (Drawable)component;
// give newly added components a sane starting location.
drawableComponent.Origin = Anchor.TopCentre;
drawableComponent.Anchor = Anchor.TopCentre;
drawableComponent.Y = targetContainer.DrawSize.Y / 2;
targetContainer.Add(component);
SelectedComponents.Clear();
SelectedComponents.Add(component);
}
private void populateSettings()
{
settingsToolbox.Clear();
var first = SelectedComponents.OfType<Drawable>().FirstOrDefault();
if (first != null)
{
settingsToolbox.Children = first.CreateSettingsControls().ToArray();
}
}
private IEnumerable<ISkinnableTarget> availableTargets => targetScreen.ChildrenOfType<ISkinnableTarget>();
private ISkinnableTarget getTarget(SkinnableTarget target)
{
return availableTargets.FirstOrDefault(c => c.Target == target);
}
private void revert()
{
ISkinnableTarget[] targetContainers = availableTargets.ToArray();
foreach (var t in targetContainers)
{
currentSkin.Value.ResetDrawableTarget(t);
// add back default components
getTarget(t.Target).Reload();
}
}
public void Save()
{
if (!hasBegunMutating)
return;
ISkinnableTarget[] targetContainers = availableTargets.ToArray();
foreach (var t in targetContainers)
currentSkin.Value.UpdateDrawableTarget(t);
skins.Save(skins.CurrentSkin.Value);
}
protected override bool OnHover(HoverEvent e) => true;
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override void PopIn()
{
this.FadeIn(TRANSITION_DURATION, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(TRANSITION_DURATION, Easing.OutQuint);
}
public void DeleteItems(ISkinnableDrawable[] items)
{
foreach (var item in items)
availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item);
}
private class SceneLibrary : CompositeDrawable
{
public const float HEIGHT = 30;
private const float padding = 10;
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
public SceneLibrary()
{
Height = HEIGHT + padding * 2;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("222")
},
new OsuScrollContainer(Direction.Horizontal)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new FillFlowContainer
{
Name = "Scene library",
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Spacing = new Vector2(padding),
Padding = new MarginPadding(padding),
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new PurpleTriangleButton
{
Text = "Song Select",
Width = 100,
Height = HEIGHT,
Action = () => game?.PerformFromScreen(screen =>
{
if (screen is SongSelect)
return;
screen.Push(new PlaySongSelect());
}, new[] { typeof(SongSelect) })
},
new PurpleTriangleButton
{
Text = "Gameplay",
Width = 100,
Height = HEIGHT,
Action = () => game?.PerformFromScreen(screen =>
{
if (screen is Player)
return;
var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod();
if (replayGeneratingMod != null)
screen.Push(new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateReplayScore(beatmap, mods)));
}, new[] { typeof(Player), typeof(SongSelect) })
},
}
},
}
}
};
}
}
}
}