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.
2021-04-30 11:35:58 +08:00
using System ;
2021-05-14 15:03:22 +08:00
using System.Collections.Generic ;
2023-01-26 16:46:19 +08:00
using System.Diagnostics ;
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 ;
2023-02-15 18:26:44 +08:00
using Newtonsoft.Json ;
2023-11-11 19:34:35 +08:00
using osu.Framework ;
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 ;
2023-02-28 04:34:07 +08:00
using Web = osu . Game . Resources . Localisation . Web ;
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 ;
2023-02-27 03:38:50 +08:00
using osu.Game.Overlays.Dialog ;
2022-07-20 04:18:19 +08:00
using osu.Game.Overlays.OSD ;
2023-02-17 14:02:42 +08:00
using osu.Game.Overlays.Settings ;
2023-02-03 17:53:22 +08:00
using osu.Game.Screens.Edit ;
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 ;
2023-01-26 17:21:04 +08:00
using osu.Game.Skinning ;
2021-04-28 14:14:48 +08:00
2023-01-26 17:21:04 +08:00
namespace osu.Game.Overlays.SkinEditor
2021-04-28 14:14:48 +08:00
{
2021-05-11 16:49:00 +08:00
[Cached(typeof(SkinEditor))]
2023-02-03 17:53:22 +08:00
public partial class SkinEditor : VisibilityContainer , ICanAcceptFiles , IKeyBindingHandler < PlatformAction > , IEditorChangeHandler
2021-04-28 14:14:48 +08:00
{
2023-02-02 17:33:45 +08:00
public const double TRANSITION_DURATION = 300 ;
2021-04-29 16:20:22 +08:00
2022-06-06 17:02:42 +08:00
public const float MENU_HEIGHT = 40 ;
2023-02-15 15:01:26 +08:00
public readonly BindableList < ISerialisableDrawable > SelectedComponents = new BindableList < ISerialisableDrawable > ( ) ;
2021-05-13 12:04:17 +08:00
protected override bool StartHidden = > true ;
2024-01-16 14:23:07 +08:00
private Drawable ? targetScreen ;
2021-04-29 12:53:01 +08:00
2023-01-26 16:46:19 +08:00
private OsuTextFlowContainer headerText = null ! ;
2021-04-29 16:26:55 +08:00
2023-01-26 16:46:19 +08:00
private Bindable < Skin > currentSkin = null ! ;
2024-04-03 16:46:26 +08:00
private Bindable < string > clipboardContent = null ! ;
2021-05-11 16:49:00 +08:00
2023-01-26 16:46:19 +08:00
[Resolved]
private OsuGame ? game { get ; set ; }
2022-03-23 14:11:35 +08:00
2021-05-10 21:43:48 +08:00
[Resolved]
2023-01-26 16:46:19 +08:00
private SkinManager skins { get ; set ; } = null ! ;
2021-05-10 21:43:48 +08:00
2021-05-11 17:07:58 +08:00
[Resolved]
2023-01-26 16:46:19 +08:00
private OsuColour colours { get ; set ; } = null ! ;
2021-05-11 16:49:00 +08:00
2022-04-01 14:49:05 +08:00
[Resolved]
2023-01-26 16:46:19 +08:00
private RealmAccess realm { get ; set ; } = null ! ;
2022-04-01 14:49:05 +08:00
2023-01-26 16:46:19 +08:00
[Resolved]
private SkinEditorOverlay ? skinEditorOverlay { get ; set ; }
2022-03-28 19:43:23 +08:00
2022-03-15 14:33:01 +08:00
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider ( OverlayColourScheme . Blue ) ;
2023-02-17 14:02:42 +08:00
private readonly Bindable < SkinComponentsContainerLookup ? > selectedTarget = new Bindable < SkinComponentsContainerLookup ? > ( ) ;
2021-05-12 16:42:04 +08:00
private bool hasBegunMutating ;
2023-01-26 16:46:19 +08:00
private Container ? content ;
2022-03-11 22:08:40 +08:00
2023-01-26 16:46:19 +08:00
private EditorSidebar componentsSidebar = null ! ;
private EditorSidebar settingsSidebar = null ! ;
2022-03-11 22:30:46 +08:00
2023-02-03 17:53:22 +08:00
private SkinEditorChangeHandler ? changeHandler ;
private EditorMenuItem undoMenuItem = null ! ;
private EditorMenuItem redoMenuItem = null ! ;
2023-02-15 18:26:44 +08:00
private EditorMenuItem cutMenuItem = null ! ;
private EditorMenuItem copyMenuItem = null ! ;
private EditorMenuItem cloneMenuItem = null ! ;
private EditorMenuItem pasteMenuItem = null ! ;
private readonly BindableWithCurrent < bool > canCut = new BindableWithCurrent < bool > ( ) ;
private readonly BindableWithCurrent < bool > canCopy = new BindableWithCurrent < bool > ( ) ;
private readonly BindableWithCurrent < bool > canPaste = new BindableWithCurrent < bool > ( ) ;
2023-01-26 16:46:19 +08:00
[Resolved]
private OnScreenDisplay ? onScreenDisplay { get ; set ; }
2022-07-20 04:18:19 +08:00
2023-02-27 03:38:50 +08:00
[Resolved]
private IDialogOverlay ? dialogOverlay { 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]
2024-04-03 17:39:12 +08:00
private void load ( EditorClipboard clipboard )
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
{
2023-01-17 00:55:28 +08:00
Name = @"Menu container" ,
2022-03-15 15:57:39 +08:00
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 [ ]
{
2023-01-17 00:39:50 +08:00
new MenuItem ( CommonStrings . MenuBarFile )
2022-03-15 15:57:39 +08:00
{
2023-11-21 13:24:10 +08:00
Items = new OsuMenuItem [ ]
2022-03-15 15:57:39 +08:00
{
2023-02-28 04:34:07 +08:00
new EditorMenuItem ( Web . CommonStrings . ButtonsSave , MenuItemType . Standard , ( ) = > Save ( ) ) ,
2023-11-11 19:34:35 +08:00
new EditorMenuItem ( CommonStrings . Export , MenuItemType . Standard , ( ) = > skins . ExportCurrentSkin ( ) ) { Action = { Disabled = ! RuntimeInfo . IsDesktop } } ,
2023-11-21 13:24:10 +08:00
new OsuMenuItemSpacer ( ) ,
2023-03-07 17:01:13 +08:00
new EditorMenuItem ( CommonStrings . RevertToDefault , MenuItemType . Destructive , ( ) = > dialogOverlay ? . Push ( new RevertConfirmDialog ( revert ) ) ) ,
2023-11-21 13:24:10 +08:00
new OsuMenuItemSpacer ( ) ,
2023-01-17 00:39:50 +08:00
new EditorMenuItem ( CommonStrings . Exit , MenuItemType . Standard , ( ) = > skinEditorOverlay ? . Hide ( ) ) ,
2022-03-15 15:57:39 +08:00
} ,
} ,
2023-02-03 17:53:22 +08:00
new MenuItem ( CommonStrings . MenuBarEdit )
{
2023-11-21 13:24:10 +08:00
Items = new OsuMenuItem [ ]
2023-02-03 17:53:22 +08:00
{
undoMenuItem = new EditorMenuItem ( CommonStrings . Undo , MenuItemType . Standard , Undo ) ,
redoMenuItem = new EditorMenuItem ( CommonStrings . Redo , MenuItemType . Standard , Redo ) ,
2023-11-21 13:24:10 +08:00
new OsuMenuItemSpacer ( ) ,
2023-02-15 18:26:44 +08:00
cutMenuItem = new EditorMenuItem ( CommonStrings . Cut , MenuItemType . Standard , Cut ) ,
copyMenuItem = new EditorMenuItem ( CommonStrings . Copy , MenuItemType . Standard , Copy ) ,
pasteMenuItem = new EditorMenuItem ( CommonStrings . Paste , MenuItemType . Standard , Paste ) ,
cloneMenuItem = new EditorMenuItem ( CommonStrings . Clone , MenuItemType . Standard , Clone ) ,
2023-02-03 17:53:22 +08:00
}
} ,
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
}
} ;
2024-04-03 17:39:12 +08:00
clipboardContent = clipboard . Content . GetBoundCopy ( ) ;
2021-05-11 17:07:58 +08:00
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2023-02-15 18:26:44 +08:00
canCut . Current . BindValueChanged ( cut = > cutMenuItem . Action . Disabled = ! cut . NewValue , true ) ;
canCopy . Current . BindValueChanged ( copy = >
{
copyMenuItem . Action . Disabled = ! copy . NewValue ;
cloneMenuItem . Action . Disabled = ! copy . NewValue ;
} , true ) ;
canPaste . Current . BindValueChanged ( paste = > pasteMenuItem . Action . Disabled = ! paste . NewValue , true ) ;
SelectedComponents . BindCollectionChanged ( ( _ , _ ) = >
{
canCopy . Value = canCut . Value = SelectedComponents . Any ( ) ;
} , true ) ;
2024-04-03 16:46:26 +08:00
clipboardContent . BindValueChanged ( content = > canPaste . Value = ! string . IsNullOrEmpty ( content . NewValue ) , true ) ;
2023-02-15 18:26:44 +08:00
2021-05-11 17:07:58 +08:00
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.
2024-04-29 20:06:23 +08:00
currentSkin . BindValueChanged ( val = >
2021-05-12 16:42:04 +08:00
{
2024-04-29 20:06:23 +08:00
if ( val . OldValue ! = null & & hasBegunMutating )
save ( val . OldValue ) ;
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 ) ;
2023-02-17 14:50:48 +08:00
selectedTarget . BindValueChanged ( targetChanged , 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 )
{
2023-02-15 18:26:44 +08:00
case PlatformAction . Cut :
Cut ( ) ;
return true ;
case PlatformAction . Copy :
Copy ( ) ;
return true ;
case PlatformAction . Paste :
Paste ( ) ;
return true ;
2023-02-03 17:53:22 +08:00
case PlatformAction . Undo :
Undo ( ) ;
return true ;
case PlatformAction . Redo :
Redo ( ) ;
return true ;
2022-07-22 12:10:18 +08:00
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 ;
2023-02-03 17:53:22 +08:00
changeHandler ? . Dispose ( ) ;
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.
2023-03-04 20:49:10 +08:00
if ( content ? . Child is SkinBlueprintContainer )
content . Clear ( ) ;
2023-01-26 16:46:19 +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 ( )
{
2023-02-17 14:50:48 +08:00
selectedTarget . Default = getFirstTarget ( ) ? . Lookup ;
if ( ! availableTargets . Any ( t = > t . Lookup . Equals ( selectedTarget . Value ) ) )
2023-02-21 12:50:19 +08:00
selectedTarget . SetDefault ( ) ;
2022-03-15 17:57:53 +08:00
}
2022-03-11 22:08:40 +08:00
}
2023-02-17 14:50:48 +08:00
private void targetChanged ( ValueChangedEvent < SkinComponentsContainerLookup ? > target )
{
foreach ( var toolbox in componentsSidebar . OfType < SkinComponentToolbox > ( ) )
toolbox . Expire ( ) ;
2023-03-04 21:09:58 +08:00
componentsSidebar . Clear ( ) ;
2023-03-04 20:18:34 +08:00
SelectedComponents . Clear ( ) ;
2023-02-17 14:50:48 +08:00
2023-02-17 15:57:36 +08:00
Debug . Assert ( content ! = null ) ;
var skinComponentsContainer = getTarget ( target . NewValue ) ;
2023-03-04 20:49:10 +08:00
if ( target . NewValue = = null | | skinComponentsContainer = = null )
{
content . Child = new NonSkinnableScreenPlaceholder ( ) ;
2023-02-17 15:57:36 +08:00
return ;
2023-03-04 20:49:10 +08:00
}
2023-02-17 15:57:36 +08:00
changeHandler = new SkinEditorChangeHandler ( skinComponentsContainer ) ;
changeHandler . CanUndo . BindValueChanged ( v = > undoMenuItem . Action . Disabled = ! v . NewValue , true ) ;
changeHandler . CanRedo . BindValueChanged ( v = > redoMenuItem . Action . Disabled = ! v . NewValue , true ) ;
content . Child = new SkinBlueprintContainer ( skinComponentsContainer ) ;
componentsSidebar . Children = new [ ]
{
new EditorSidebarSection ( "Current working layer" )
{
Children = new Drawable [ ]
{
new SettingsDropdown < SkinComponentsContainerLookup ? >
{
2023-07-31 14:10:58 +08:00
Items = availableTargets . Select ( t = > t . Lookup ) . Distinct ( ) ,
2023-02-17 15:57:36 +08:00
Current = selectedTarget ,
}
}
} ,
} ;
2023-02-17 14:50:48 +08:00
// If the new target has a ruleset, let's show ruleset-specific items at the top, and the rest below.
if ( target . NewValue . Ruleset ! = null )
{
2023-07-28 14:48:40 +08:00
componentsSidebar . Add ( new SkinComponentToolbox ( skinComponentsContainer , target . NewValue . Ruleset )
2023-02-17 14:50:48 +08:00
{
2023-02-22 04:38:51 +08:00
RequestPlacement = requestPlacement
2023-02-17 14:50:48 +08:00
} ) ;
}
// Remove the ruleset from the lookup to get base components.
2023-07-28 14:48:40 +08:00
componentsSidebar . Add ( new SkinComponentToolbox ( skinComponentsContainer , null )
2023-02-17 14:50:48 +08:00
{
2023-02-22 04:38:51 +08:00
RequestPlacement = requestPlacement
2023-02-17 14:50:48 +08:00
} ) ;
2023-02-22 04:38:51 +08:00
void requestPlacement ( Type type )
{
if ( ! ( Activator . CreateInstance ( type ) is ISerialisableDrawable component ) )
throw new InvalidOperationException ( $"Attempted to instantiate a component for placement which was not an {typeof(ISerialisableDrawable)}." ) ;
SelectedComponents . Clear ( ) ;
placeComponent ( component ) ;
2022-03-15 17:57:53 +08:00
}
2023-02-17 14:50:48 +08:00
}
2021-05-11 17:07:58 +08:00
private void skinChanged ( )
{
headerText . Clear ( ) ;
2023-01-17 00:55:28 +08:00
headerText . AddParagraph ( SkinEditorStrings . SkinEditor , cp = > cp . Font = OsuFont . Default . With ( size : 16 ) ) ;
2021-05-11 17:07:58 +08:00
headerText . NewParagraph ( ) ;
2023-02-02 07:44:00 +08:00
headerText . AddText ( SkinEditorStrings . CurrentlyEditing , 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
2023-02-02 07:44:00 +08:00
headerText . AddText ( $" {currentSkin.Value.SkinInfo}" , cp = >
2021-05-11 17:07:58 +08:00
{
cp . Font = OsuFont . Default . With ( size : 12 , weight : FontWeight . Bold ) ;
cp . Colour = colours . Yellow ;
} ) ;
2023-11-11 16:02:21 +08:00
changeHandler ? . Dispose ( ) ;
2021-05-11 17:07:58 +08:00
skins . EnsureMutableSkin ( ) ;
2023-11-11 16:02:21 +08:00
var targetContainer = getTarget ( selectedTarget . Value ) ;
if ( targetContainer ! = null )
changeHandler = new SkinEditorChangeHandler ( targetContainer ) ;
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
2023-02-20 19:00:12 +08:00
/// <summary>
2023-02-21 13:07:47 +08:00
/// Attempt to place a given component in the current target. If successful, the new component will be added to <see cref="SelectedComponents"/>.
2023-02-20 19:00:12 +08:00
/// </summary>
/// <param name="component">The component to be placed.</param>
/// <param name="applyDefaults">Whether to apply default anchor / origin / position values.</param>
/// <returns>Whether placement succeeded. Could fail if no target is available, or if the current target has missing dependency requirements for the component.</returns>
private bool placeComponent ( ISerialisableDrawable component , bool applyDefaults = true )
2021-04-30 11:35:58 +08:00
{
2023-02-17 14:50:48 +08:00
var targetContainer = getTarget ( selectedTarget . Value ) ;
2021-05-12 13:11:40 +08:00
if ( targetContainer = = null )
2023-02-20 19:00:12 +08:00
return false ;
2021-05-12 13:11:40 +08:00
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
2023-02-20 19:00:12 +08:00
try
{
targetContainer . Add ( component ) ;
}
catch
{
// May fail if dependencies are not available, for instance.
return false ;
}
2021-05-11 16:49:00 +08:00
2021-05-12 13:02:20 +08:00
SelectedComponents . Add ( component ) ;
2024-04-12 15:52:36 +08:00
SkinSelectionHandler . ApplyClosestAnchorOrigin ( drawableComponent ) ;
2023-02-20 19:00:12 +08:00
return true ;
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
}
2023-02-15 17:31:55 +08:00
private IEnumerable < SkinComponentsContainer > availableTargets = > targetScreen . ChildrenOfType < SkinComponentsContainer > ( ) ;
2021-05-14 15:03:22 +08:00
2023-02-17 14:02:42 +08:00
private SkinComponentsContainer ? getFirstTarget ( ) = > availableTargets . FirstOrDefault ( ) ;
2022-03-15 17:57:53 +08:00
2023-02-17 14:50:48 +08:00
private SkinComponentsContainer ? getTarget ( SkinComponentsContainerLookup ? target )
2021-05-11 10:57:12 +08:00
{
2023-02-17 14:50:48 +08:00
return availableTargets . FirstOrDefault ( c = > c . Lookup . Equals ( target ) ) ;
2021-04-30 11:35:58 +08:00
}
2021-05-11 10:57:12 +08:00
private void revert ( )
{
2023-02-15 17:31:55 +08:00
SkinComponentsContainer [ ] 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
2023-02-17 14:50:48 +08:00
getTarget ( t . Lookup ) ? . Reload ( ) ;
2021-05-11 10:57:12 +08:00
}
}
2023-02-15 18:26:44 +08:00
protected void Cut ( )
{
Copy ( ) ;
DeleteItems ( SelectedComponents . ToArray ( ) ) ;
}
protected void Copy ( )
{
2024-04-03 17:39:12 +08:00
clipboardContent . Value = JsonConvert . SerializeObject ( SelectedComponents . Cast < Drawable > ( ) . Select ( s = > s . CreateSerialisedInfo ( ) ) . ToArray ( ) ) ;
2023-02-15 18:26:44 +08:00
}
protected void Clone ( )
{
// Avoid attempting to clone if copying is not available (as it may result in pasting something unexpected).
if ( ! canCopy . Value )
return ;
Copy ( ) ;
Paste ( ) ;
}
protected void Paste ( )
{
2023-11-23 08:55:27 +08:00
if ( ! canPaste . Value )
return ;
2023-02-15 18:35:22 +08:00
changeHandler ? . BeginChange ( ) ;
2024-04-03 17:39:12 +08:00
var drawableInfo = JsonConvert . DeserializeObject < SerialisedDrawableInfo [ ] > ( clipboardContent . Value ) ;
2023-02-15 18:26:44 +08:00
if ( drawableInfo = = null )
return ;
var instances = drawableInfo . Select ( d = > d . CreateInstance ( ) )
. OfType < ISerialisableDrawable > ( )
. ToArray ( ) ;
SelectedComponents . Clear ( ) ;
2023-02-20 19:02:43 +08:00
foreach ( var i in instances )
2023-02-21 13:07:47 +08:00
placeComponent ( i , false ) ;
2023-02-15 18:35:22 +08:00
changeHandler ? . EndChange ( ) ;
2023-02-15 18:26:44 +08:00
}
2023-02-03 17:53:22 +08:00
protected void Undo ( ) = > changeHandler ? . RestoreState ( - 1 ) ;
protected void Redo ( ) = > changeHandler ? . RestoreState ( 1 ) ;
2024-06-11 17:31:30 +08:00
void IEditorChangeHandler . RestoreState ( int direction ) = > changeHandler ? . RestoreState ( direction ) ;
2024-05-07 03:52:03 +08:00
public void Save ( bool userTriggered = true ) = > save ( currentSkin . Value , userTriggered ) ;
2024-04-29 20:06:23 +08:00
private void save ( Skin skin , bool userTriggered = true )
2021-05-10 21:43:48 +08:00
{
2021-05-12 16:42:04 +08:00
if ( ! hasBegunMutating )
return ;
2024-01-16 14:23:07 +08:00
if ( targetScreen ? . IsLoaded ! = true )
return ;
2023-02-15 17:31:55 +08:00
SkinComponentsContainer [ ] targetContainers = availableTargets . ToArray ( ) ;
2021-05-10 21:43:48 +08:00
2024-01-16 14:23:07 +08:00
if ( ! targetContainers . All ( c = > c . ComponentsLoaded ) )
return ;
2021-05-10 21:43:48 +08:00
foreach ( var t in targetContainers )
2024-04-29 20:06:23 +08:00
skin . UpdateDrawableTarget ( t ) ;
2021-05-10 21:43:48 +08:00
2023-02-02 17:42:33 +08:00
// In the case the save was user triggered, always show the save message to make them feel confident.
2024-04-29 20:06:23 +08:00
if ( skins . Save ( skin ) | | userTriggered )
onScreenDisplay ? . Display ( new SkinEditorToast ( ToastStrings . SkinSaved , skin . SkinInfo . ToString ( ) ? ? "Unknown" ) ) ;
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 ( )
{
2023-02-03 01:41:35 +08:00
this . 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
2023-02-15 15:01:26 +08:00
public void DeleteItems ( ISerialisableDrawable [ ] items )
2021-05-14 15:03:22 +08:00
{
2023-02-15 18:35:22 +08:00
changeHandler ? . BeginChange ( ) ;
2021-05-15 04:33:26 +08:00
foreach ( var item in items )
2023-02-22 16:45:38 +08:00
availableTargets . FirstOrDefault ( t = > t . Components . Contains ( item ) ) ? . Remove ( item , true ) ;
2023-02-15 18:35:22 +08:00
changeHandler ? . EndChange ( ) ;
2021-05-14 15:03:22 +08:00
}
2022-03-23 14:11:35 +08:00
2023-02-22 16:45:38 +08:00
public void BringSelectionToFront ( )
{
if ( getTarget ( selectedTarget . Value ) is not SkinComponentsContainer target )
return ;
2023-02-23 19:03:36 +08:00
changeHandler ? . BeginChange ( ) ;
2023-02-22 16:45:38 +08:00
// Iterating by target components order ensures we maintain the same order across selected components, regardless
// of the order they were selected in.
foreach ( var d in target . Components . ToArray ( ) )
{
if ( ! SelectedComponents . Contains ( d ) )
continue ;
target . Remove ( d , false ) ;
// Selection would be reset by the remove.
SelectedComponents . Add ( d ) ;
target . Add ( d ) ;
}
2023-02-23 19:03:36 +08:00
changeHandler ? . EndChange ( ) ;
2023-02-22 16:45:38 +08:00
}
public void SendSelectionToBack ( )
{
if ( getTarget ( selectedTarget . Value ) is not SkinComponentsContainer target )
return ;
2023-02-23 19:03:36 +08:00
changeHandler ? . BeginChange ( ) ;
2023-02-22 16:45:38 +08:00
foreach ( var d in target . Components . ToArray ( ) )
{
if ( SelectedComponents . Contains ( d ) )
continue ;
target . Remove ( d , false ) ;
target . Add ( d ) ;
}
2023-02-15 18:35:22 +08:00
changeHandler ? . EndChange ( ) ;
2021-05-14 15:03:22 +08:00
}
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 ( ) ) ;
2023-01-26 16:46:19 +08:00
var skinnableTarget = getFirstTarget ( ) ;
// Import still should happen for now, even if not placeable (as it allows a user to import skin resources that would apply to legacy gameplay skins).
if ( skinnableTarget = = null )
return ;
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 ,
2024-05-27 17:23:32 +08:00
Position = skinnableTarget . ToLocalSpace ( GetContainingInputManager ( ) ! . CurrentState . Mouse . Position ) ,
2022-04-01 15:16:49 +08:00
} ;
2023-02-21 13:07:47 +08:00
SelectedComponents . Clear ( ) ;
2022-04-01 15:16:49 +08:00
placeComponent ( sprite , false ) ;
2022-03-23 14:11:35 +08:00
} ) ;
return Task . CompletedTask ;
}
2022-12-13 20:03:25 +08:00
Task ICanAcceptFiles . Import ( ImportTask [ ] tasks , ImportParameters parameters ) = > throw new NotImplementedException ( ) ;
2022-03-23 14:11:35 +08:00
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
2022-11-24 13:32:20 +08:00
private partial class SkinEditorToast : Toast
2022-07-20 04:18:19 +08:00
{
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
}
2023-02-03 17:53:22 +08:00
2023-03-07 17:01:13 +08:00
public partial class RevertConfirmDialog : DangerousActionDialog
2023-02-27 03:38:50 +08:00
{
2023-03-07 17:01:13 +08:00
public RevertConfirmDialog ( Action revert )
2023-02-27 03:38:50 +08:00
{
2023-03-07 17:07:53 +08:00
HeaderText = CommonStrings . RevertToDefault ;
BodyText = SkinEditorStrings . RevertToDefaultDescription ;
2023-03-06 03:57:26 +08:00
DangerousAction = revert ;
2023-02-27 03:38:50 +08:00
}
}
2023-02-03 17:53:22 +08:00
#region Delegation of IEditorChangeHandler
public event Action ? OnStateChange
{
2023-02-07 15:04:31 +08:00
add = > throw new NotImplementedException ( ) ;
remove = > throw new NotImplementedException ( ) ;
2023-02-03 17:53:22 +08:00
}
2023-02-07 15:07:33 +08:00
private IEditorChangeHandler ? beginChangeHandler ;
public void BeginChange ( )
{
// Change handler may change between begin and end, which can cause unbalanced operations.
// Let's track the one that was used when beginning the change so we can call EndChange on it specifically.
( beginChangeHandler = changeHandler ) ? . BeginChange ( ) ;
}
public void EndChange ( ) = > beginChangeHandler ? . EndChange ( ) ;
2023-02-03 17:53:22 +08:00
public void SaveState ( ) = > changeHandler ? . SaveState ( ) ;
#endregion
2021-04-28 14:14:48 +08:00
}
}