2021-04-28 14:14:48 +08:00
// 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.
2022-06-17 15:37:17 +08:00
#nullable disable
2021-04-30 11:35:58 +08:00
using System ;
2021-05-14 15:03:22 +08:00
using System.Collections.Generic ;
2022-03-23 14:11:35 +08:00
using System.IO ;
2021-04-30 11:35:58 +08:00
using System.Linq ;
2022-03-23 14:11:35 +08:00
using System.Threading.Tasks ;
2021-04-29 12:53:01 +08:00
using osu.Framework.Allocation ;
2021-05-11 16:00:56 +08:00
using osu.Framework.Bindables ;
2021-04-28 14:14:48 +08:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
2022-03-02 19:05:37 +08:00
using osu.Framework.Graphics.UserInterface ;
2022-07-22 12:10:18 +08:00
using osu.Framework.Input ;
using osu.Framework.Input.Bindings ;
2021-04-29 16:20:22 +08:00
using osu.Framework.Input.Events ;
2022-07-20 04:18:19 +08:00
using osu.Framework.Localisation ;
2021-04-30 11:35:58 +08:00
using osu.Framework.Testing ;
2022-03-23 14:11:35 +08:00
using osu.Game.Database ;
2021-04-29 16:26:55 +08:00
using osu.Game.Graphics ;
using osu.Game.Graphics.Containers ;
2021-04-28 14:14:48 +08:00
using osu.Game.Graphics.Cursor ;
2021-05-10 21:43:48 +08:00
using osu.Game.Graphics.UserInterface ;
2022-07-20 04:18:19 +08:00
using osu.Game.Localisation ;
2022-03-15 14:33:01 +08:00
using osu.Game.Overlays ;
2022-07-20 04:18:19 +08:00
using osu.Game.Overlays.OSD ;
2022-03-15 15:36:04 +08:00
using osu.Game.Screens.Edit.Components ;
2022-03-02 19:05:37 +08:00
using osu.Game.Screens.Edit.Components.Menus ;
2021-04-28 14:14:48 +08:00
namespace osu.Game.Skinning.Editor
{
2021-05-11 16:49:00 +08:00
[Cached(typeof(SkinEditor))]
2022-07-22 12:10:18 +08:00
public class SkinEditor : VisibilityContainer , ICanAcceptFiles , IKeyBindingHandler < PlatformAction >
2021-04-28 14:14:48 +08:00
{
2021-04-29 16:20:22 +08:00
public const double TRANSITION_DURATION = 500 ;
2022-06-06 17:02:42 +08:00
public const float MENU_HEIGHT = 40 ;
2021-05-13 16:06:00 +08:00
public readonly BindableList < ISkinnableDrawable > SelectedComponents = new BindableList < ISkinnableDrawable > ( ) ;
2021-05-13 12:04:17 +08:00
protected override bool StartHidden = > true ;
2022-03-11 22:08:40 +08:00
private Drawable targetScreen ;
2021-04-29 12:53:01 +08:00
2021-04-29 16:26:55 +08:00
private OsuTextFlowContainer headerText ;
2021-05-13 12:04:17 +08:00
private Bindable < Skin > currentSkin ;
2021-05-11 16:49:00 +08:00
2022-03-23 14:11:35 +08:00
[Resolved(canBeNull: true)]
private OsuGame game { get ; set ; }
2021-05-10 21:43:48 +08:00
[Resolved]
private SkinManager skins { get ; set ; }
2021-05-11 17:07:58 +08:00
[Resolved]
private OsuColour colours { get ; set ; }
2021-05-11 16:49:00 +08:00
2022-04-01 14:49:05 +08:00
[Resolved]
private RealmAccess realm { get ; set ; }
2022-03-28 19:43:23 +08:00
[Resolved(canBeNull: true)]
private SkinEditorOverlay skinEditorOverlay { get ; set ; }
2022-03-15 14:33:01 +08:00
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider ( OverlayColourScheme . Blue ) ;
2021-05-12 16:42:04 +08:00
private bool hasBegunMutating ;
2022-03-13 15:10:06 +08:00
private Container content ;
2022-03-11 22:08:40 +08:00
2022-03-15 17:57:53 +08:00
private EditorSidebar componentsSidebar ;
2022-03-15 16:41:20 +08:00
private EditorSidebar settingsSidebar ;
2022-03-11 22:30:46 +08:00
2022-07-20 04:18:19 +08:00
[Resolved(canBeNull: true)]
private OnScreenDisplay onScreenDisplay { get ; set ; }
2022-03-15 17:00:32 +08:00
public SkinEditor ( )
2021-04-28 14:14:48 +08:00
{
2022-03-15 17:00:32 +08:00
}
2022-03-11 22:08:40 +08:00
2022-03-15 17:00:32 +08:00
public SkinEditor ( Drawable targetScreen )
{
2022-03-11 22:08:40 +08:00
UpdateTargetScreen ( targetScreen ) ;
2021-04-28 14:14:48 +08:00
}
2021-04-29 12:53:01 +08:00
[BackgroundDependencyLoader]
2021-05-11 17:07:58 +08:00
private void load ( )
2021-04-28 14:14:48 +08:00
{
2022-03-15 17:00:32 +08:00
RelativeSizeAxes = Axes . Both ;
2021-04-28 14:14:48 +08:00
InternalChild = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes . Both ,
2022-03-15 15:57:39 +08:00
Child = new GridContainer
2021-04-28 14:14:48 +08:00
{
2022-03-15 15:57:39 +08:00
RelativeSizeAxes = Axes . Both ,
RowDimensions = new [ ]
2021-04-29 16:26:55 +08:00
{
2022-03-15 15:57:39 +08:00
new Dimension ( GridSizeMode . AutoSize ) ,
new Dimension ( GridSizeMode . AutoSize ) ,
new Dimension ( ) ,
} ,
Content = new [ ]
{
new Drawable [ ]
2022-03-02 19:05:37 +08:00
{
2022-03-15 15:57:39 +08:00
new Container
2022-03-02 19:05:37 +08:00
{
2022-03-15 15:57:39 +08:00
Name = "Menu container" ,
RelativeSizeAxes = Axes . X ,
Depth = float . MinValue ,
2022-06-06 17:02:42 +08:00
Height = MENU_HEIGHT ,
2022-03-15 15:57:39 +08:00
Children = new Drawable [ ]
2022-03-02 19:05:37 +08:00
{
2022-03-15 15:57:39 +08:00
new EditorMenuBar
2022-03-02 19:05:37 +08:00
{
2022-03-15 15:57:39 +08:00
Anchor = Anchor . CentreLeft ,
Origin = Anchor . CentreLeft ,
RelativeSizeAxes = Axes . Both ,
2022-03-02 19:05:37 +08:00
Items = new [ ]
{
2022-03-15 15:57:39 +08:00
new MenuItem ( "File" )
{
Items = new [ ]
{
new EditorMenuItem ( "Save" , MenuItemType . Standard , Save ) ,
new EditorMenuItem ( "Revert to default" , MenuItemType . Destructive , revert ) ,
new EditorMenuItemSpacer ( ) ,
2022-03-28 19:43:23 +08:00
new EditorMenuItem ( "Exit" , MenuItemType . Standard , ( ) = > skinEditorOverlay ? . Hide ( ) ) ,
2022-03-15 15:57:39 +08:00
} ,
} ,
}
2022-03-02 19:05:37 +08:00
} ,
2022-03-15 15:57:39 +08:00
headerText = new OsuTextFlowContainer
{
TextAnchor = Anchor . TopRight ,
Padding = new MarginPadding ( 5 ) ,
Anchor = Anchor . TopRight ,
Origin = Anchor . TopRight ,
AutoSizeAxes = Axes . X ,
RelativeSizeAxes = Axes . Y ,
} ,
} ,
2022-03-02 19:05:37 +08:00
} ,
} ,
2022-03-15 15:57:39 +08:00
new Drawable [ ]
2021-05-10 21:43:48 +08:00
{
2022-03-15 15:57:39 +08:00
new SkinEditorSceneLibrary
{
RelativeSizeAxes = Axes . X ,
} ,
2021-05-10 21:43:48 +08:00
} ,
2022-03-15 15:57:39 +08:00
new Drawable [ ]
2021-05-11 10:57:12 +08:00
{
2022-03-15 15:57:39 +08:00
new GridContainer
2021-05-11 10:57:12 +08:00
{
2022-03-15 15:57:39 +08:00
RelativeSizeAxes = Axes . Both ,
ColumnDimensions = new [ ]
2021-05-11 17:37:41 +08:00
{
2022-03-15 15:57:39 +08:00
new Dimension ( GridSizeMode . AutoSize ) ,
new Dimension ( ) ,
new Dimension ( GridSizeMode . AutoSize ) ,
2021-05-11 17:37:41 +08:00
} ,
2022-03-15 15:57:39 +08:00
Content = new [ ]
2022-03-11 22:30:46 +08:00
{
2022-03-15 15:57:39 +08:00
new Drawable [ ]
2022-03-15 15:36:04 +08:00
{
2022-03-15 17:57:53 +08:00
componentsSidebar = new EditorSidebar ( ) ,
2022-03-15 15:57:39 +08:00
content = new Container
{
Depth = float . MaxValue ,
RelativeSizeAxes = Axes . Both ,
} ,
2022-03-15 16:41:20 +08:00
settingsSidebar = new EditorSidebar ( ) ,
2022-03-15 15:36:04 +08:00
}
2022-03-15 15:57:39 +08:00
}
2021-05-11 17:37:41 +08:00
}
2022-03-15 15:57:39 +08:00
} ,
2021-05-11 17:37:41 +08:00
}
2021-04-28 14:14:48 +08:00
}
} ;
2021-05-11 17:07:58 +08:00
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
Show ( ) ;
2021-04-29 16:26:55 +08:00
2022-03-23 14:11:35 +08:00
game ? . RegisterImportHandler ( this ) ;
2021-05-11 17:07:58 +08:00
// 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.
2022-06-24 20:25:23 +08:00
currentSkin . BindValueChanged ( _ = >
2021-05-12 16:42:04 +08:00
{
hasBegunMutating = false ;
Scheduler . AddOnce ( skinChanged ) ;
} , true ) ;
2022-03-11 22:30:46 +08:00
2022-06-24 20:25:23 +08:00
SelectedComponents . BindCollectionChanged ( ( _ , _ ) = > Scheduler . AddOnce ( populateSettings ) , true ) ;
2021-05-11 17:07:58 +08:00
}
2022-07-22 12:10:18 +08:00
public bool OnPressed ( KeyBindingPressEvent < PlatformAction > e )
{
switch ( e . Action )
{
case PlatformAction . Save :
if ( e . Repeat )
return false ;
Save ( ) ;
return true ;
}
return false ;
}
public void OnReleased ( KeyBindingReleaseEvent < PlatformAction > e )
{
}
2022-03-11 22:08:40 +08:00
public void UpdateTargetScreen ( Drawable targetScreen )
{
this . targetScreen = targetScreen ;
2022-03-11 22:30:46 +08:00
SelectedComponents . Clear ( ) ;
2022-03-15 17:00:32 +08:00
2022-04-07 18:12:10 +08:00
// Immediately clear the previous blueprint container to ensure it doesn't try to interact with the old target.
2022-04-07 22:42:42 +08:00
content ? . Clear ( ) ;
2022-04-07 18:12:10 +08:00
2022-03-11 22:08:40 +08:00
Scheduler . AddOnce ( loadBlueprintContainer ) ;
2022-03-15 15:57:39 +08:00
Scheduler . AddOnce ( populateSettings ) ;
2022-03-11 22:08:40 +08:00
2022-03-15 17:57:53 +08:00
void loadBlueprintContainer ( )
{
content . Child = new SkinBlueprintContainer ( targetScreen ) ;
componentsSidebar . Child = new SkinComponentToolbox ( getFirstTarget ( ) as CompositeDrawable )
{
RequestPlacement = placeComponent
} ;
}
2022-03-11 22:08:40 +08:00
}
2021-05-11 17:07:58 +08:00
private void skinChanged ( )
{
headerText . Clear ( ) ;
2022-03-02 19:05:37 +08:00
headerText . AddParagraph ( "Skin editor" , cp = > cp . Font = OsuFont . Default . With ( size : 16 ) ) ;
2021-05-11 17:07:58 +08:00
headerText . NewParagraph ( ) ;
headerText . AddText ( "Currently editing " , cp = >
2021-04-29 16:26:55 +08:00
{
cp . Font = OsuFont . Default . With ( size : 12 ) ;
cp . Colour = colours . Yellow ;
} ) ;
2021-05-11 17:07:58 +08:00
headerText . AddText ( $"{currentSkin.Value.SkinInfo}" , cp = >
{
cp . Font = OsuFont . Default . With ( size : 12 , weight : FontWeight . Bold ) ;
cp . Colour = colours . Yellow ;
} ) ;
skins . EnsureMutableSkin ( ) ;
2021-05-12 16:42:04 +08:00
hasBegunMutating = true ;
2021-04-28 14:14:48 +08:00
}
2021-04-29 12:53:01 +08:00
2021-04-30 11:35:58 +08:00
private void placeComponent ( Type type )
2022-03-23 14:11:35 +08:00
{
if ( ! ( Activator . CreateInstance ( type ) is ISkinnableDrawable component ) )
throw new InvalidOperationException ( $"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}." ) ;
placeComponent ( component ) ;
}
2022-04-01 15:16:49 +08:00
private void placeComponent ( ISkinnableDrawable component , bool applyDefaults = true )
2021-04-30 11:35:58 +08:00
{
2022-03-15 17:57:53 +08:00
var targetContainer = getFirstTarget ( ) ;
2021-05-12 13:11:40 +08:00
if ( targetContainer = = null )
return ;
var drawableComponent = ( Drawable ) component ;
2022-04-01 15:16:49 +08:00
if ( applyDefaults )
{
// give newly added components a sane starting location.
drawableComponent . Origin = Anchor . TopCentre ;
drawableComponent . Anchor = Anchor . TopCentre ;
drawableComponent . Y = targetContainer . DrawSize . Y / 2 ;
}
2021-04-30 11:35:58 +08:00
2021-05-12 13:11:40 +08:00
targetContainer . Add ( component ) ;
2021-05-11 16:49:00 +08:00
SelectedComponents . Clear ( ) ;
2021-05-12 13:02:20 +08:00
SelectedComponents . Add ( component ) ;
2021-05-11 10:57:12 +08:00
}
2021-04-30 11:35:58 +08:00
2022-03-15 17:00:32 +08:00
private void populateSettings ( )
2022-03-11 22:30:46 +08:00
{
2022-03-15 16:41:20 +08:00
settingsSidebar . Clear ( ) ;
2022-03-11 22:30:46 +08:00
2022-03-15 16:41:20 +08:00
foreach ( var component in SelectedComponents . OfType < Drawable > ( ) )
settingsSidebar . Add ( new SkinSettingsToolbox ( component ) ) ;
2022-03-11 22:30:46 +08:00
}
2021-05-14 15:03:22 +08:00
private IEnumerable < ISkinnableTarget > availableTargets = > targetScreen . ChildrenOfType < ISkinnableTarget > ( ) ;
2022-03-15 17:57:53 +08:00
private ISkinnableTarget getFirstTarget ( ) = > availableTargets . FirstOrDefault ( ) ;
2022-11-09 15:04:56 +08:00
private ISkinnableTarget getTarget ( GlobalSkinComponentLookup . LookupType target )
2021-05-11 10:57:12 +08:00
{
2021-05-14 15:03:22 +08:00
return availableTargets . FirstOrDefault ( c = > c . Target = = target ) ;
2021-04-30 11:35:58 +08:00
}
2021-05-11 10:57:12 +08:00
private void revert ( )
{
2021-05-14 15:03:22 +08:00
ISkinnableTarget [ ] targetContainers = availableTargets . ToArray ( ) ;
2021-05-11 10:57:12 +08:00
foreach ( var t in targetContainers )
{
2021-05-11 16:00:56 +08:00
currentSkin . Value . ResetDrawableTarget ( t ) ;
2021-05-11 10:57:12 +08:00
// add back default components
getTarget ( t . Target ) . Reload ( ) ;
}
}
2021-05-12 15:07:00 +08:00
public void Save ( )
2021-05-10 21:43:48 +08:00
{
2021-05-12 16:42:04 +08:00
if ( ! hasBegunMutating )
return ;
2021-05-14 15:03:22 +08:00
ISkinnableTarget [ ] targetContainers = availableTargets . ToArray ( ) ;
2021-05-10 21:43:48 +08:00
foreach ( var t in targetContainers )
2021-05-11 16:00:56 +08:00
currentSkin . Value . UpdateDrawableTarget ( t ) ;
2021-05-10 21:43:48 +08:00
skins . Save ( skins . CurrentSkin . Value ) ;
2022-07-22 08:46:17 +08:00
onScreenDisplay ? . Display ( new SkinEditorToast ( ToastStrings . SkinSaved , currentSkin . Value . SkinInfo . ToString ( ) ) ) ;
2021-05-10 21:43:48 +08:00
}
2021-04-29 16:20:22 +08:00
protected override bool OnHover ( HoverEvent e ) = > true ;
protected override bool OnMouseDown ( MouseDownEvent e ) = > true ;
2021-04-29 12:53:01 +08:00
2022-06-20 02:34:52 +08:00
public override void Hide ( )
{
base . Hide ( ) ;
SelectedComponents . Clear ( ) ;
}
2021-04-29 12:53:01 +08:00
protected override void PopIn ( )
{
2022-06-06 17:06:46 +08:00
this
// align animation to happen after the majority of the ScalingContainer animation completes.
. Delay ( ScalingContainer . TRANSITION_DURATION * 0.3f )
. FadeIn ( TRANSITION_DURATION , Easing . OutQuint ) ;
2021-04-29 12:53:01 +08:00
}
protected override void PopOut ( )
{
2021-04-29 16:20:22 +08:00
this . FadeOut ( TRANSITION_DURATION , Easing . OutQuint ) ;
2021-04-29 12:53:01 +08:00
}
2021-05-14 15:03:22 +08:00
public void DeleteItems ( ISkinnableDrawable [ ] items )
{
2021-05-15 04:33:26 +08:00
foreach ( var item in items )
2021-05-14 15:03:22 +08:00
availableTargets . FirstOrDefault ( t = > t . Components . Contains ( item ) ) ? . Remove ( item ) ;
}
2022-03-23 14:11:35 +08:00
2022-04-04 19:30:14 +08:00
#region Drag & drop import handling
2022-03-23 14:11:35 +08:00
public Task Import ( params string [ ] paths )
{
Schedule ( ( ) = >
{
var file = new FileInfo ( paths . First ( ) ) ;
// import to skin
currentSkin . Value . SkinInfo . PerformWrite ( skinInfo = >
{
using ( var contents = file . OpenRead ( ) )
skins . AddFile ( skinInfo , contents , file . Name ) ;
} ) ;
2022-04-01 14:49:05 +08:00
// Even though we are 100% on an update thread, we need to wait for realm callbacks to fire (to correctly invalidate caches in RealmBackedResourceStore).
// See https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-2483573 for further discussion.
// This is the best we can do for now.
realm . Run ( r = > r . Refresh ( ) ) ;
2022-03-23 14:11:35 +08:00
// place component
2022-04-01 15:16:49 +08:00
var sprite = new SkinnableSprite
2022-03-23 14:11:35 +08:00
{
2022-04-01 15:16:49 +08:00
SpriteName = { Value = file . Name } ,
Origin = Anchor . Centre ,
Position = getFirstTarget ( ) . ToLocalSpace ( GetContainingInputManager ( ) . CurrentState . Mouse . Position ) ,
} ;
placeComponent ( sprite , false ) ;
SkinSelectionHandler . ApplyClosestAnchor ( sprite ) ;
2022-03-23 14:11:35 +08:00
} ) ;
return Task . CompletedTask ;
}
public Task Import ( params ImportTask [ ] tasks ) = > throw new NotImplementedException ( ) ;
public IEnumerable < string > HandledExtensions = > new [ ] { ".jpg" , ".jpeg" , ".png" } ;
2022-04-04 19:30:14 +08:00
#endregion
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
game ? . UnregisterImportHandler ( this ) ;
}
2022-07-20 04:18:19 +08:00
private class SkinEditorToast : Toast
{
public SkinEditorToast ( LocalisableString value , string skinDisplayName )
2022-07-22 13:00:29 +08:00
: base ( SkinSettingsStrings . SkinLayoutEditor , value , skinDisplayName )
{
}
2022-07-20 04:18:19 +08:00
}
2021-04-28 14:14:48 +08:00
}
}