mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Profile editor - Ported most of the property tree
This commit is contained in:
parent
126540e2f3
commit
0b905cca2e
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DocumentationFile>C:\Repos\Artemis\src\Artemis.UI.Avalonia.Shared\Artemis.UI.Avalonia.Shared.xml</DocumentationFile>
|
||||
<DocumentationFile>bin\Artemis.UI.Avalonia.Shared.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Artemis.UI.Avalonia.Shared.csproj.DotSettings" />
|
||||
@ -43,7 +43,7 @@
|
||||
<Compile Update="Controls\HotkeyBox.axaml.cs">
|
||||
<DependentUpon>HotkeyBox.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Services\WindowService\ExceptionDialogView.axaml.cs">
|
||||
<Compile Update="Services\Window\ExceptionDialogView.axaml.cs">
|
||||
<DependentUpon>%(Filename)</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=exceptions/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindow/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindows/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cwindowservice/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core.LayerBrushes;
|
||||
|
||||
namespace Artemis.UI.Shared.LayerBrushes
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a view model for a brush configuration window
|
||||
/// </summary>
|
||||
public abstract class BrushConfigurationViewModel : ActivatableViewModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="BrushConfigurationViewModel" /> class
|
||||
/// </summary>
|
||||
/// <param name="layerBrush"></param>
|
||||
protected BrushConfigurationViewModel(BaseLayerBrush layerBrush)
|
||||
{
|
||||
LayerBrush = layerBrush;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the layer brush this view model is associated with
|
||||
/// </summary>
|
||||
public BaseLayerBrush LayerBrush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Closes the dialog
|
||||
/// </summary>
|
||||
public void RequestClose()
|
||||
{
|
||||
CloseRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the window wants to close, returning <see langword="false" /> will cause the window to stay open.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true" /> if the window may close; otherwise <see langword="false" />.</returns>
|
||||
public virtual bool CanClose()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the window wants to close, returning <see langword="false" /> will cause the window to stay open.
|
||||
/// </summary>
|
||||
/// <returns>A task <see langword="true" /> if the window may close; otherwise <see langword="false" />.</returns>
|
||||
public virtual Task<bool> CanCloseAsync()
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a close was requested
|
||||
/// </summary>
|
||||
public event EventHandler? CloseRequested;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using Artemis.Core.LayerBrushes;
|
||||
|
||||
namespace Artemis.UI.Shared.LayerBrushes
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class LayerBrushConfigurationDialog<T> : LayerBrushConfigurationDialog where T : BrushConfigurationViewModel
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public LayerBrushConfigurationDialog()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LayerBrushConfigurationDialog(int dialogWidth, int dialogHeight)
|
||||
{
|
||||
DialogWidth = dialogWidth;
|
||||
DialogHeight = dialogHeight;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type Type => typeof(T);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a UI tab for a layer brush
|
||||
/// </summary>
|
||||
public abstract class LayerBrushConfigurationDialog : ILayerBrushConfigurationDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// The default width of the dialog
|
||||
/// </summary>
|
||||
public int DialogWidth { get; set; } = 800;
|
||||
|
||||
/// <summary>
|
||||
/// The default height of the dialog
|
||||
/// </summary>
|
||||
public int DialogHeight { get; set; } = 800;
|
||||
|
||||
/// <summary>
|
||||
/// The type of view model the dialog contains
|
||||
/// </summary>
|
||||
public abstract Type Type { get; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core.LayerEffects;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace Artemis.UI.Shared.LayerEffects;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a view model for an effect configuration window
|
||||
/// </summary>
|
||||
public abstract class EffectConfigurationViewModel : ActivatableViewModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="EffectConfigurationViewModel" /> class
|
||||
/// </summary>
|
||||
/// <param name="layerEffect"></param>
|
||||
protected EffectConfigurationViewModel(BaseLayerEffect layerEffect)
|
||||
{
|
||||
LayerEffect = layerEffect;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the layer effect this view model is associated with
|
||||
/// </summary>
|
||||
public BaseLayerEffect LayerEffect { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Closes the dialog
|
||||
/// </summary>
|
||||
public void RequestClose()
|
||||
{
|
||||
CloseRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the window wants to close, returning <see langword="false" /> will cause the window to stay open.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true" /> if the window may close; otherwise <see langword="false" />.</returns>
|
||||
public virtual bool CanClose()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the window wants to close, returning <see langword="false" /> will cause the window to stay open.
|
||||
/// </summary>
|
||||
/// <returns>A task <see langword="true" /> if the window may close; otherwise <see langword="false" />.</returns>
|
||||
public virtual Task<bool> CanCloseAsync()
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a close was requested
|
||||
/// </summary>
|
||||
public event EventHandler? CloseRequested;
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using Artemis.Core.LayerEffects;
|
||||
|
||||
namespace Artemis.UI.Shared.LayerEffects
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class LayerEffectConfigurationDialog<T> : LayerEffectConfigurationDialog where T : EffectConfigurationViewModel
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public LayerEffectConfigurationDialog()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LayerEffectConfigurationDialog(int dialogWidth, int dialogHeight)
|
||||
{
|
||||
DialogWidth = dialogWidth;
|
||||
DialogHeight = dialogHeight;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type Type => typeof(T);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a UI tab for a specific layer effect
|
||||
/// </summary>
|
||||
public abstract class LayerEffectConfigurationDialog : ILayerEffectConfigurationDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// The default width of the dialog
|
||||
/// </summary>
|
||||
public int DialogWidth { get; set; } = 800;
|
||||
|
||||
/// <summary>
|
||||
/// The default height of the dialog
|
||||
/// </summary>
|
||||
public int DialogHeight { get; set; } = 800;
|
||||
|
||||
/// <summary>
|
||||
/// The type of view model the dialog contains
|
||||
/// </summary>
|
||||
public abstract Type Type { get; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
using Artemis.Core.ScriptingProviders;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Shared.ScriptingProviders
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Stylet view model containing a script editor
|
||||
/// </summary>
|
||||
public class ScriptEditorViewModel : ActivatableViewModelBase, IScriptEditorViewModel
|
||||
{
|
||||
private Script? _script;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ScriptEditorViewModel" />
|
||||
/// </summary>
|
||||
/// <param name="scriptType">The script type this view model was created for</param>
|
||||
public ScriptEditorViewModel(ScriptType scriptType)
|
||||
{
|
||||
ScriptType = scriptType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called just before the script is changed to a different one
|
||||
/// </summary>
|
||||
/// <param name="script">The script to display or <see langword="null" /> if no script is to be displayed</param>
|
||||
protected virtual void OnScriptChanging(Script? script)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after the script was changed to a different one
|
||||
/// </summary>
|
||||
/// <param name="script">The script to display or <see langword="null" /> if no script is to be displayed</param>
|
||||
protected virtual void OnScriptChanged(Script? script)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ScriptType ScriptType { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Script? Script
|
||||
{
|
||||
get => _script;
|
||||
internal set => this.RaiseAndSetIfChanged(ref _script, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ChangeScript(Script? script)
|
||||
{
|
||||
OnScriptChanging(script);
|
||||
Script = script;
|
||||
OnScriptChanged(script);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.MainWindowService
|
||||
namespace Artemis.UI.Shared.Services.MainWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a class that provides the main window, so that <see cref="IMainWindowService" /> can control the state of
|
||||
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.MainWindowService
|
||||
namespace Artemis.UI.Shared.Services.MainWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// A service that can be used to manage the state of the main window.
|
||||
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.MainWindowService
|
||||
namespace Artemis.UI.Shared.Services.MainWindow
|
||||
{
|
||||
internal class MainWindowService : IMainWindowService
|
||||
{
|
||||
@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using Artemis.Core;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a profile editor command that can be used to add a profile element.
|
||||
/// </summary>
|
||||
public class AddProfileElement : IProfileEditorCommand, IDisposable
|
||||
{
|
||||
private readonly int _index;
|
||||
private readonly RenderProfileElement _subject;
|
||||
private readonly ProfileElement _target;
|
||||
private bool _isAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="AddProfileElement"/> class.
|
||||
/// </summary>
|
||||
public AddProfileElement(RenderProfileElement subject, ProfileElement target, int index)
|
||||
{
|
||||
_subject = subject;
|
||||
_target = target;
|
||||
_index = index;
|
||||
|
||||
DisplayName = subject switch
|
||||
{
|
||||
Layer => "Add layer",
|
||||
Folder => "Add folder",
|
||||
_ => throw new ArgumentException("Type of subject is not supported")
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_isAdded)
|
||||
_subject.Dispose();
|
||||
}
|
||||
|
||||
#region Implementation of IProfileEditorCommand
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
_isAdded = true;
|
||||
_target.AddChild(_subject, _index);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
_isAdded = false;
|
||||
_target.RemoveChild(_subject);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using Artemis.Core;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a profile editor command that can be used to remove a profile element.
|
||||
/// </summary>
|
||||
public class RemoveProfileElement : IProfileEditorCommand, IDisposable
|
||||
{
|
||||
private readonly int _index;
|
||||
private readonly RenderProfileElement _subject;
|
||||
private readonly ProfileElement _target;
|
||||
private bool _isRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="RemoveProfileElement"/> class.
|
||||
/// </summary>
|
||||
public RemoveProfileElement(RenderProfileElement subject)
|
||||
{
|
||||
if (subject.Parent == null)
|
||||
throw new ArtemisSharedUIException("Can't remove a subject that has no parent");
|
||||
|
||||
_subject = subject;
|
||||
_target = _subject.Parent;
|
||||
_index = _subject.Children.IndexOf(_subject);
|
||||
|
||||
DisplayName = subject switch
|
||||
{
|
||||
Layer => "Remove layer",
|
||||
Folder => "Remove folder",
|
||||
_ => throw new ArgumentException("Type of subject is not supported")
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isRemoved)
|
||||
_subject.Dispose();
|
||||
}
|
||||
|
||||
#region Implementation of IProfileEditorCommand
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
_isRemoved = true;
|
||||
_target.RemoveChild(_subject);
|
||||
_subject.Deactivate();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
_isRemoved = false;
|
||||
_subject.Activate();
|
||||
_target.AddChild(_subject, _index);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using Artemis.Core;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a profile editor command that can be used to update a layer property of type <typeparamref name="T" />.
|
||||
/// </summary>
|
||||
public class UpdateLayerProperty<T> : IProfileEditorCommand
|
||||
{
|
||||
private readonly LayerProperty<T> _layerProperty;
|
||||
private readonly T _newValue;
|
||||
private readonly T _originalValue;
|
||||
private readonly TimeSpan? _time;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="UpdateLayerProperty{T}" /> class.
|
||||
/// </summary>
|
||||
public UpdateLayerProperty(LayerProperty<T> layerProperty, T newValue, TimeSpan? time)
|
||||
{
|
||||
_layerProperty = layerProperty;
|
||||
_originalValue = layerProperty.CurrentValue;
|
||||
_newValue = newValue;
|
||||
_time = time;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="UpdateLayerProperty{T}" /> class.
|
||||
/// </summary>
|
||||
public UpdateLayerProperty(LayerProperty<T> layerProperty, T newValue, T originalValue, TimeSpan? time)
|
||||
{
|
||||
_layerProperty = layerProperty;
|
||||
_originalValue = originalValue;
|
||||
_newValue = newValue;
|
||||
_time = time;
|
||||
}
|
||||
|
||||
#region Implementation of IProfileEditorCommand
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName => $"Update {_layerProperty.PropertyDescription.Name ?? "property"}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
_layerProperty.SetCurrentValue(_newValue, _time);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
_layerProperty.SetCurrentValue(_originalValue, _time);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
namespace Artemis.UI.Services.ProfileEditor
|
||||
namespace Artemis.UI.Shared.Services.ProfileEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a command that can be executed and if needed, undone
|
||||
@ -1,17 +1,23 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
|
||||
namespace Artemis.UI.Services.ProfileEditor
|
||||
namespace Artemis.UI.Shared.Services.ProfileEditor
|
||||
{
|
||||
public interface IProfileEditorService : IArtemisUIService
|
||||
public interface IProfileEditorService : IArtemisSharedUIService
|
||||
{
|
||||
IObservable<ProfileConfiguration?> ProfileConfiguration { get; }
|
||||
IObservable<RenderProfileElement?> ProfileElement { get; }
|
||||
IObservable<ProfileEditorHistory?> History { get; }
|
||||
IObservable<TimeSpan> Time { get; }
|
||||
|
||||
void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration);
|
||||
void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement);
|
||||
void ChangeTime(TimeSpan time);
|
||||
|
||||
void ExecuteCommand(IProfileEditorCommand command);
|
||||
void SaveProfile();
|
||||
Task SaveProfileAsync();
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ using System.Reactive.Subjects;
|
||||
using Artemis.Core;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Services.ProfileEditor
|
||||
namespace Artemis.UI.Shared.Services.ProfileEditor
|
||||
{
|
||||
public class ProfileEditorHistory
|
||||
{
|
||||
@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.ProfileEditor;
|
||||
|
||||
internal class ProfileEditorService : IProfileEditorService
|
||||
{
|
||||
private readonly BehaviorSubject<ProfileConfiguration?> _profileConfigurationSubject = new(null);
|
||||
private readonly Dictionary<ProfileConfiguration, ProfileEditorHistory> _profileEditorHistories = new();
|
||||
private readonly BehaviorSubject<RenderProfileElement?> _profileElementSubject = new(null);
|
||||
private readonly BehaviorSubject<TimeSpan> _timeSubject = new(TimeSpan.Zero);
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly IWindowService _windowService;
|
||||
|
||||
public ProfileEditorService(IProfileService profileService, IWindowService windowService)
|
||||
{
|
||||
_profileService = profileService;
|
||||
_windowService = windowService;
|
||||
ProfileConfiguration = _profileConfigurationSubject.AsObservable().DistinctUntilChanged();
|
||||
ProfileElement = _profileElementSubject.AsObservable().DistinctUntilChanged();
|
||||
History = Observable.Defer(() => Observable.Return(GetHistory(_profileConfigurationSubject.Value))).Concat(ProfileConfiguration.Select(GetHistory));
|
||||
}
|
||||
|
||||
private ProfileEditorHistory? GetHistory(ProfileConfiguration? profileConfiguration)
|
||||
{
|
||||
if (profileConfiguration == null)
|
||||
return null;
|
||||
if (_profileEditorHistories.TryGetValue(profileConfiguration, out ProfileEditorHistory? history))
|
||||
return history;
|
||||
|
||||
ProfileEditorHistory newHistory = new(profileConfiguration);
|
||||
_profileEditorHistories.Add(profileConfiguration, newHistory);
|
||||
return newHistory;
|
||||
}
|
||||
|
||||
public IObservable<ProfileConfiguration?> ProfileConfiguration { get; }
|
||||
public IObservable<RenderProfileElement?> ProfileElement { get; }
|
||||
public IObservable<ProfileEditorHistory?> History { get; }
|
||||
public IObservable<TimeSpan> Time { get; }
|
||||
|
||||
public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration)
|
||||
{
|
||||
_profileConfigurationSubject.OnNext(profileConfiguration);
|
||||
}
|
||||
|
||||
public void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement)
|
||||
{
|
||||
_profileElementSubject.OnNext(renderProfileElement);
|
||||
}
|
||||
|
||||
public void ChangeTime(TimeSpan time)
|
||||
{
|
||||
_timeSubject.OnNext(time);
|
||||
}
|
||||
|
||||
public void ExecuteCommand(IProfileEditorCommand command)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProfileEditorHistory? history = GetHistory(_profileConfigurationSubject.Value);
|
||||
if (history == null)
|
||||
throw new ArtemisSharedUIException("Can't execute a command when there's no active profile configuration");
|
||||
|
||||
history.Execute.Execute(command).Subscribe();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_windowService.ShowExceptionDialog("Editor command failed", e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveProfile()
|
||||
{
|
||||
Profile? profile = _profileConfigurationSubject.Value?.Profile;
|
||||
if (profile == null)
|
||||
return;
|
||||
|
||||
_profileService.SaveProfile(profile, true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SaveProfileAsync()
|
||||
{
|
||||
await Task.Run(SaveProfile);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.PropertyInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a property input registration registered through <see cref="IPropertyInputService.RegisterPropertyInput"/>
|
||||
/// </summary>
|
||||
public class PropertyInputRegistration
|
||||
{
|
||||
private readonly IPropertyInputService _propertyInputService;
|
||||
|
||||
internal PropertyInputRegistration(IPropertyInputService propertyInputService, Plugin plugin, Type supportedType, Type viewModelType)
|
||||
{
|
||||
_propertyInputService = propertyInputService;
|
||||
Plugin = plugin;
|
||||
SupportedType = supportedType;
|
||||
ViewModelType = viewModelType;
|
||||
|
||||
if (Plugin != Constants.CorePlugin)
|
||||
Plugin.Disabled += InstanceOnDisabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin that registered the property input
|
||||
/// </summary>
|
||||
public Plugin Plugin { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type supported by the property input
|
||||
/// </summary>
|
||||
public Type SupportedType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the view model type of the property input
|
||||
/// </summary>
|
||||
public Type ViewModelType { get; }
|
||||
|
||||
internal void Unsubscribe()
|
||||
{
|
||||
if (Plugin != Constants.CorePlugin)
|
||||
Plugin.Disabled -= InstanceOnDisabled;
|
||||
}
|
||||
|
||||
private void InstanceOnDisabled(object? sender, EventArgs e)
|
||||
{
|
||||
// Profile editor service will call Unsubscribe
|
||||
_propertyInputService.RemovePropertyInput(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.PropertyInput
|
||||
{
|
||||
internal class PropertyInputService : IPropertyInputService
|
||||
{
|
||||
private readonly List<PropertyInputRegistration> _registeredPropertyEditors;
|
||||
|
||||
public PropertyInputService()
|
||||
{
|
||||
_registeredPropertyEditors = new List<PropertyInputRegistration>();
|
||||
RegisteredPropertyEditors = new ReadOnlyCollection<PropertyInputRegistration>(_registeredPropertyEditors);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReadOnlyCollection<PropertyInputRegistration> RegisteredPropertyEditors { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public PropertyInputRegistration RegisterPropertyInput<T>(Plugin plugin) where T : PropertyInputViewModel
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RemovePropertyInput(PropertyInputRegistration registration)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public PropertyInputViewModel<T>? CreatePropertyInputViewModel<T>(LayerProperty<T> layerProperty)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public interface IPropertyInputService : IArtemisSharedUIService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a read-only collection of all registered property editors
|
||||
/// </summary>
|
||||
ReadOnlyCollection<PropertyInputRegistration> RegisteredPropertyEditors { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new property input view model used in the profile editor for the generic type defined in
|
||||
/// <see cref="PropertyInputViewModel{T}" />
|
||||
/// <para>Note: DataBindingProperty will remove itself on plugin disable so you don't have to</para>
|
||||
/// </summary>
|
||||
/// <param name="plugin"></param>
|
||||
/// <returns></returns>
|
||||
PropertyInputRegistration RegisterPropertyInput<T>(Plugin plugin) where T : PropertyInputViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new property input view model used in the profile editor for the generic type defined in
|
||||
/// <see cref="PropertyInputViewModel{T}" />
|
||||
/// <para>Note: DataBindingProperty will remove itself on plugin disable so you don't have to</para>
|
||||
/// </summary>
|
||||
/// <param name="viewModelType"></param>
|
||||
/// <param name="plugin"></param>
|
||||
/// <returns></returns>
|
||||
PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the property input view model
|
||||
/// </summary>
|
||||
/// <param name="registration"></param>
|
||||
void RemovePropertyInput(PropertyInputRegistration registration);
|
||||
|
||||
/// <summary>
|
||||
/// Determines if there is a matching registration for the provided layer property
|
||||
/// </summary>
|
||||
/// <param name="layerProperty">The layer property to try to find a view model for</param>
|
||||
bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty);
|
||||
|
||||
/// <summary>
|
||||
/// If a matching registration is found, creates a new <see cref="PropertyInputViewModel{T}" /> supporting
|
||||
/// <typeparamref name="T" />
|
||||
/// </summary>
|
||||
PropertyInputViewModel<T>? CreatePropertyInputViewModel<T>(LayerProperty<T> layerProperty);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,213 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.PropertyInput;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the base class for a property input view model that is used to edit layer properties
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of property this input view model supports</typeparam>
|
||||
public abstract class PropertyInputViewModel<T> : PropertyInputViewModel
|
||||
{
|
||||
[AllowNull]
|
||||
private T _inputValue;
|
||||
private bool _inputDragging;
|
||||
private T _dragStartValue;
|
||||
private TimeSpan _time;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="PropertyInputViewModel{T}" /> class
|
||||
/// </summary>
|
||||
protected PropertyInputViewModel(LayerProperty<T> layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService)
|
||||
{
|
||||
LayerProperty = layerProperty;
|
||||
ProfileEditorService = profileEditorService;
|
||||
PropertyInputService = propertyInputService;
|
||||
|
||||
_inputValue = default!;
|
||||
_dragStartValue = default!;
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
ProfileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
|
||||
UpdateInputValue();
|
||||
|
||||
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.Updated += x, x => LayerProperty.Updated -= x)
|
||||
.Subscribe(_ => UpdateInputValue())
|
||||
.DisposeWith(d);
|
||||
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.CurrentValueSet += x, x => LayerProperty.CurrentValueSet -= x)
|
||||
.Subscribe(_ => UpdateInputValue())
|
||||
.DisposeWith(d);
|
||||
Observable.FromEventPattern<DataBindingEventArgs>(x => LayerProperty.DataBinding.DataBindingEnabled += x, x => LayerProperty.DataBinding.DataBindingEnabled -= x)
|
||||
.Subscribe(_ => UpdateDataBinding())
|
||||
.DisposeWith(d);
|
||||
Observable.FromEventPattern<DataBindingEventArgs>(x => LayerProperty.DataBinding.DataBindingDisabled += x, x => LayerProperty.DataBinding.DataBindingDisabled -= x)
|
||||
.Subscribe(_ => UpdateDataBinding())
|
||||
.DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the layer property this view model is editing
|
||||
/// </summary>
|
||||
public LayerProperty<T> LayerProperty { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether the layer property should be enabled
|
||||
/// </summary>
|
||||
public bool IsEnabled => !LayerProperty.HasDataBinding;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the profile editor service
|
||||
/// </summary>
|
||||
public IProfileEditorService ProfileEditorService { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property input service
|
||||
/// </summary>
|
||||
public IPropertyInputService PropertyInputService { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether the input is currently being dragged
|
||||
/// <para>
|
||||
/// Only applicable when using something like a <see cref="DraggableFloat" />, see
|
||||
/// <see cref="InputDragStarted" /> and <see cref="InputDragEnded" />
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public bool InputDragging
|
||||
{
|
||||
get => _inputDragging;
|
||||
private set => this.RaiseAndSetIfChanged(ref _inputDragging, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the input value
|
||||
/// </summary>
|
||||
[AllowNull]
|
||||
public T InputValue
|
||||
{
|
||||
get => _inputValue;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _inputValue, value);
|
||||
ApplyInputValue();
|
||||
}
|
||||
}
|
||||
|
||||
internal override object InternalGuard { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Called by the view input drag has started
|
||||
/// <para>
|
||||
/// To use, add the following to DraggableFloat in your xaml: <c>DragStarted="{s:Action InputDragStarted}"</c>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void InputDragStarted(object sender, EventArgs e)
|
||||
{
|
||||
InputDragging = true;
|
||||
_dragStartValue = GetDragStartValue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the view when input drag has ended
|
||||
/// <para>
|
||||
/// To use, add the following to DraggableFloat in your xaml: <c>DragEnded="{s:Action InputDragEnded}"</c>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void InputDragEnded(object sender, EventArgs e)
|
||||
{
|
||||
InputDragging = false;
|
||||
ProfileEditorService.ExecuteCommand(new UpdateLayerProperty<T>(LayerProperty, _inputValue, _dragStartValue, _time));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the input value has been applied to the layer property
|
||||
/// </summary>
|
||||
protected virtual void OnInputValueApplied()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the input value has changed
|
||||
/// </summary>
|
||||
protected virtual void OnInputValueChanged()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when data bindings have been enabled or disabled on the layer property
|
||||
/// </summary>
|
||||
protected virtual void OnDataBindingsChanged()
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual T GetDragStartValue()
|
||||
{
|
||||
return InputValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the input value to the layer property
|
||||
/// </summary>
|
||||
protected void ApplyInputValue()
|
||||
{
|
||||
OnInputValueChanged();
|
||||
LayerProperty.SetCurrentValue(_inputValue, _time);
|
||||
OnInputValueApplied();
|
||||
|
||||
if (InputDragging)
|
||||
ProfileEditorService.ChangeTime(_time);
|
||||
else
|
||||
ProfileEditorService.ExecuteCommand(new UpdateLayerProperty<T>(LayerProperty, _inputValue, _time));
|
||||
}
|
||||
|
||||
private void UpdateInputValue()
|
||||
{
|
||||
// Avoid unnecessary UI updates and validator cycles
|
||||
if (_inputValue != null && _inputValue.Equals(LayerProperty.CurrentValue) || _inputValue == null && LayerProperty.CurrentValue == null)
|
||||
return;
|
||||
|
||||
// Override the input value
|
||||
_inputValue = LayerProperty.CurrentValue;
|
||||
|
||||
// Notify a change in the input value
|
||||
OnInputValueChanged();
|
||||
this.RaisePropertyChanged(nameof(InputValue));
|
||||
}
|
||||
|
||||
private void UpdateDataBinding()
|
||||
{
|
||||
this.RaisePropertyChanged(nameof(IsEnabled));
|
||||
OnDataBindingsChanged();
|
||||
}
|
||||
|
||||
private void LayerPropertyOnUpdated(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateInputValue();
|
||||
}
|
||||
|
||||
private void OnDataBindingChange(object? sender, DataBindingEventArgs e)
|
||||
{
|
||||
this.RaisePropertyChanged(nameof(IsEnabled));
|
||||
OnDataBindingsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For internal use only, implement <see cref="PropertyInputViewModel" /> instead.
|
||||
/// </summary>
|
||||
public abstract class PropertyInputViewModel : ActivatableViewModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Prevents this type being implemented directly, implement
|
||||
/// <see cref="PropertyInputViewModel" /> instead.
|
||||
/// </summary>
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
internal abstract object InternalGuard { get; }
|
||||
}
|
||||
@ -51,6 +51,12 @@
|
||||
<Compile Update="Screens\Debugger\Tabs\Settings\DebugSettingsView.axaml.cs">
|
||||
<DependentUpon>DebugSettingsView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Screens\ProfileEditor\Panels\ProfileElementProperties\ProfileElementPropertiesView.axaml.cs">
|
||||
<DependentUpon>ProfileElementPropertiesView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Screens\ProfileEditor\Panels\ProfileElementProperties\Windows\BrushConfigurationWindowView.axaml.cs">
|
||||
<DependentUpon>BrushConfigurationWindowView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Screens\Sidebar\ContentDialogs\SidebarCategoryEditView.axaml.cs">
|
||||
<DependentUpon>SidebarCategoryEditView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||
using Avalonia;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace Artemis.UI.Converters;
|
||||
|
||||
public class PropertyTreeMarginConverter : IValueConverter
|
||||
{
|
||||
public double Length { get; set; }
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is TreeGroupViewModel treeGroupViewModel)
|
||||
return new Thickness(Length * treeGroupViewModel.GetDepth(), 0, 0, 0);
|
||||
// TODO
|
||||
// if (value is ITreePropertyViewModel treePropertyViewModel)
|
||||
// return new Thickness(Length * treePropertyViewModel.GetDepth(), 0, 0, 0);
|
||||
|
||||
return new Thickness(0);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@ -3,11 +3,14 @@ using Artemis.Core;
|
||||
using Artemis.UI.Screens.Device;
|
||||
using Artemis.UI.Screens.Plugins;
|
||||
using Artemis.UI.Screens.ProfileEditor;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileTree;
|
||||
using Artemis.UI.Screens.Settings;
|
||||
using Artemis.UI.Screens.Sidebar;
|
||||
using Artemis.UI.Screens.SurfaceEditor;
|
||||
using Artemis.UI.Services;
|
||||
using DynamicData.Binding;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Ninject.Factories
|
||||
@ -58,4 +61,18 @@ namespace Artemis.UI.Ninject.Factories
|
||||
FolderTreeItemViewModel FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder);
|
||||
LayerTreeItemViewModel LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer);
|
||||
}
|
||||
|
||||
public interface ILayerPropertyVmFactory : IVmFactory
|
||||
{
|
||||
ProfileElementPropertyViewModel ProfileElementPropertyViewModel(ILayerProperty layerProperty);
|
||||
ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup);
|
||||
|
||||
TreeGroupViewModel TreeGroupViewModel(ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel);
|
||||
// TimelineGroupViewModel TimelineGroupViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel);
|
||||
|
||||
// TreeViewModel TreeViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
|
||||
// EffectsViewModel EffectsViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel);
|
||||
// TimelineViewModel TimelineViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
|
||||
// TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
using System;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Services.ProfileEditor;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Commands
|
||||
{
|
||||
public class AddProfileElement : IProfileEditorCommand, IDisposable
|
||||
{
|
||||
private readonly int _index;
|
||||
private readonly RenderProfileElement _subject;
|
||||
private readonly ProfileElement _target;
|
||||
private bool _isAdded;
|
||||
|
||||
public AddProfileElement(RenderProfileElement subject, ProfileElement target, int index)
|
||||
{
|
||||
_subject = subject;
|
||||
_target = target;
|
||||
_index = index;
|
||||
|
||||
DisplayName = subject switch
|
||||
{
|
||||
Layer => "Add layer",
|
||||
Folder => "Add folder",
|
||||
_ => throw new ArgumentException("Type of subject is not supported")
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_isAdded)
|
||||
_subject.Dispose();
|
||||
}
|
||||
|
||||
#region Implementation of IProfileEditorCommand
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
_isAdded = true;
|
||||
_target.AddChild(_subject, _index);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
_isAdded = false;
|
||||
_target.RemoveChild(_subject);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
using System;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Exceptions;
|
||||
using Artemis.UI.Services.ProfileEditor;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Commands
|
||||
{
|
||||
public class RemoveProfileElement : IProfileEditorCommand, IDisposable
|
||||
{
|
||||
private readonly int _index;
|
||||
private readonly RenderProfileElement _subject;
|
||||
private readonly ProfileElement _target;
|
||||
private bool _isRemoved;
|
||||
|
||||
public RemoveProfileElement(RenderProfileElement subject)
|
||||
{
|
||||
if (subject.Parent == null)
|
||||
throw new ArtemisUIException("Can't remove a subject that has no parent");
|
||||
|
||||
_subject = subject;
|
||||
_target = _subject.Parent;
|
||||
_index = _subject.Children.IndexOf(_subject);
|
||||
|
||||
DisplayName = subject switch
|
||||
{
|
||||
Layer => "Remove layer",
|
||||
Folder => "Remove folder",
|
||||
_ => throw new ArgumentException("Type of subject is not supported")
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isRemoved)
|
||||
_subject.Dispose();
|
||||
}
|
||||
|
||||
#region Implementation of IProfileEditorCommand
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
_isRemoved = true;
|
||||
_target.RemoveChild(_subject);
|
||||
_subject.Deactivate();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
_isRemoved = false;
|
||||
_subject.Activate();
|
||||
_target.AddChild(_subject, _index);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.Panels.MenuBar.MenuBarView">
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.MenuBar.MenuBarView">
|
||||
<Menu Grid.Row="0" Grid.Column="0" Margin="0 2" VerticalAlignment="Top">
|
||||
<MenuItem Header="_File">
|
||||
<MenuItem Header="New">
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Panels.MenuBar
|
||||
namespace Artemis.UI.Screens.ProfileEditor.MenuBar
|
||||
{
|
||||
public partial class MenuBarView : ReactiveUserControl<MenuBarViewModel>
|
||||
{
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using Artemis.UI.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Panels.MenuBar
|
||||
namespace Artemis.UI.Screens.ProfileEditor.MenuBar
|
||||
{
|
||||
public class MenuBarViewModel : ActivatableViewModelBase
|
||||
{
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileElementProperties"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.ProfileElementPropertiesView">
|
||||
<ItemsControl Items="{Binding PropertyGroupViewModels}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<TreeDataTemplate DataType="{x:Type local:ProfileElementPropertyGroupViewModel}" ItemsSource="{Binding Children}">
|
||||
<ContentControl Content="{Binding TreeGroupViewModel}" />
|
||||
</TreeDataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</UserControl>
|
||||
@ -0,0 +1,18 @@
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties
|
||||
{
|
||||
public partial class ProfileElementPropertiesView : ReactiveUserControl<ProfileElementPropertiesViewModel>
|
||||
{
|
||||
public ProfileElementPropertiesView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.LayerEffects;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
|
||||
|
||||
public class ProfileElementPropertiesViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private ProfileElementPropertyGroupViewModel? _brushPropertyGroup;
|
||||
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ProfileElementPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
_layerPropertyVmFactory = layerPropertyVmFactory;
|
||||
PropertyGroupViewModels = new ObservableCollection<ProfileElementPropertyGroupViewModel>();
|
||||
|
||||
// Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940
|
||||
this.WhenAnyValue(x => x.ProfileElement)
|
||||
.Select(p => p is Layer l
|
||||
? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x)
|
||||
: Observable.Never<EventPattern<object>>())
|
||||
.Switch()
|
||||
.Subscribe(_ => ApplyEffects());
|
||||
this.WhenAnyValue(x => x.ProfileElement)
|
||||
.Select(p => p != null
|
||||
? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x)
|
||||
: Observable.Never<EventPattern<object>>())
|
||||
.Switch()
|
||||
.Subscribe(_ => ApplyLayerBrush());
|
||||
|
||||
// React to service profile element changes as long as the VM is active
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
_profileElement = _profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d);
|
||||
_profileEditorService.ProfileElement.Subscribe(p => PopulateProperties(p)).DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public RenderProfileElement? ProfileElement => _profileElement?.Value;
|
||||
public Layer? Layer => _profileElement?.Value as Layer;
|
||||
public ObservableCollection<ProfileElementPropertyGroupViewModel> PropertyGroupViewModels { get; }
|
||||
|
||||
private void PopulateProperties(RenderProfileElement? renderProfileElement)
|
||||
{
|
||||
PropertyGroupViewModels.Clear();
|
||||
_brushPropertyGroup = null;
|
||||
|
||||
if (ProfileElement == null)
|
||||
return;
|
||||
|
||||
// Add layer root groups
|
||||
if (Layer != null)
|
||||
{
|
||||
PropertyGroupViewModels.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(Layer.General));
|
||||
PropertyGroupViewModels.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(Layer.Transform));
|
||||
ApplyLayerBrush(false);
|
||||
}
|
||||
|
||||
ApplyEffects();
|
||||
}
|
||||
|
||||
private void ApplyLayerBrush(bool sortProperties = true)
|
||||
{
|
||||
if (Layer == null)
|
||||
return;
|
||||
|
||||
bool hideRenderRelatedProperties = Layer.LayerBrush != null && Layer.LayerBrush.SupportsTransformation;
|
||||
|
||||
Layer.General.ShapeType.IsHidden = hideRenderRelatedProperties;
|
||||
Layer.General.BlendMode.IsHidden = hideRenderRelatedProperties;
|
||||
Layer.Transform.IsHidden = hideRenderRelatedProperties;
|
||||
|
||||
if (_brushPropertyGroup != null)
|
||||
{
|
||||
PropertyGroupViewModels.Remove(_brushPropertyGroup);
|
||||
_brushPropertyGroup = null;
|
||||
}
|
||||
|
||||
if (Layer.LayerBrush?.BaseProperties != null)
|
||||
{
|
||||
_brushPropertyGroup = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(Layer.LayerBrush.BaseProperties);
|
||||
PropertyGroupViewModels.Add(_brushPropertyGroup);
|
||||
}
|
||||
|
||||
if (sortProperties)
|
||||
SortProperties();
|
||||
}
|
||||
|
||||
private void ApplyEffects(bool sortProperties = true)
|
||||
{
|
||||
if (ProfileElement == null)
|
||||
return;
|
||||
|
||||
// Remove VMs of effects no longer applied on the layer
|
||||
List<ProfileElementPropertyGroupViewModel> toRemove = PropertyGroupViewModels
|
||||
.Where(l => l.LayerPropertyGroup.LayerEffect != null && !ProfileElement.LayerEffects.Contains(l.LayerPropertyGroup.LayerEffect))
|
||||
.ToList();
|
||||
foreach (ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel in toRemove)
|
||||
PropertyGroupViewModels.Remove(profileElementPropertyGroupViewModel);
|
||||
|
||||
foreach (BaseLayerEffect layerEffect in ProfileElement.LayerEffects)
|
||||
{
|
||||
if (PropertyGroupViewModels.Any(l => l.LayerPropertyGroup.LayerEffect == layerEffect) || layerEffect.BaseProperties == null)
|
||||
continue;
|
||||
|
||||
PropertyGroupViewModels.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerEffect.BaseProperties));
|
||||
}
|
||||
|
||||
if (sortProperties)
|
||||
SortProperties();
|
||||
}
|
||||
|
||||
private void SortProperties()
|
||||
{
|
||||
// Get all non-effect properties
|
||||
List<ProfileElementPropertyGroupViewModel> nonEffectProperties = PropertyGroupViewModels
|
||||
.Where(l => l.TreeGroupViewModel.GroupType != LayerPropertyGroupType.LayerEffectRoot)
|
||||
.ToList();
|
||||
// Order the effects
|
||||
List<ProfileElementPropertyGroupViewModel> effectProperties = PropertyGroupViewModels
|
||||
.Where(l => l.TreeGroupViewModel.GroupType == LayerPropertyGroupType.LayerEffectRoot && l.LayerPropertyGroup.LayerEffect != null)
|
||||
.OrderBy(l => l.LayerPropertyGroup.LayerEffect?.Order)
|
||||
.ToList();
|
||||
|
||||
// Put the non-effect properties in front
|
||||
for (int index = 0; index < nonEffectProperties.Count; index++)
|
||||
{
|
||||
ProfileElementPropertyGroupViewModel layerPropertyGroupViewModel = nonEffectProperties[index];
|
||||
if (PropertyGroupViewModels.IndexOf(layerPropertyGroupViewModel) != index)
|
||||
PropertyGroupViewModels.Move(PropertyGroupViewModels.IndexOf(layerPropertyGroupViewModel), index);
|
||||
}
|
||||
|
||||
// Put the effect properties after, sorted by their order
|
||||
for (int index = 0; index < effectProperties.Count; index++)
|
||||
{
|
||||
ProfileElementPropertyGroupViewModel layerPropertyGroupViewModel = effectProperties[index];
|
||||
if (PropertyGroupViewModels.IndexOf(layerPropertyGroupViewModel) != index + nonEffectProperties.Count)
|
||||
PropertyGroupViewModels.Move(PropertyGroupViewModels.IndexOf(layerPropertyGroupViewModel), index + nonEffectProperties.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||
using Artemis.UI.Shared;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
|
||||
|
||||
public class ProfileElementPropertyGroupViewModel : ViewModelBase
|
||||
{
|
||||
private bool _isVisible;
|
||||
private bool _isExpanded;
|
||||
|
||||
public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory)
|
||||
{
|
||||
Children = new ObservableCollection<ActivatableViewModelBase>();
|
||||
LayerPropertyGroup = layerPropertyGroup;
|
||||
TreeGroupViewModel = layerPropertyVmFactory.TreeGroupViewModel(this);
|
||||
|
||||
IsVisible = !LayerPropertyGroup.IsHidden;
|
||||
// TODO: Update visiblity on change, can't do it atm because not sure how to unsubscribe from the event
|
||||
}
|
||||
|
||||
public ObservableCollection<ActivatableViewModelBase> Children { get; }
|
||||
public LayerPropertyGroup LayerPropertyGroup { get; }
|
||||
public TreeGroupViewModel TreeGroupViewModel { get; }
|
||||
|
||||
public bool IsVisible
|
||||
{
|
||||
get => _isVisible;
|
||||
set => this.RaiseAndSetIfChanged(ref _isVisible, value);
|
||||
}
|
||||
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => _isExpanded;
|
||||
set => this.RaiseAndSetIfChanged(ref _isExpanded, value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
|
||||
|
||||
public class ProfileElementPropertyViewModel
|
||||
{
|
||||
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
|
||||
public ProfileElementPropertyViewModel(ILayerProperty layerProperty, IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory)
|
||||
{
|
||||
LayerProperty = layerProperty;
|
||||
_profileEditorService = profileEditorService;
|
||||
_layerPropertyVmFactory = layerPropertyVmFactory;
|
||||
}
|
||||
|
||||
public ILayerProperty LayerProperty { get; }
|
||||
}
|
||||
@ -0,0 +1,183 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
|
||||
xmlns:viewModel="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree"
|
||||
xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
|
||||
xmlns:profileElementProperties="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileElementProperties"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree.TreeGroupView">
|
||||
<UserControl.Resources>
|
||||
<converters:PropertyTreeMarginConverter x:Key="PropertyTreeMarginConverter" />
|
||||
<sharedConverters:EnumToBooleanConverter x:Key="EnumBoolConverter" />
|
||||
</UserControl.Resources>
|
||||
<StackPanel>
|
||||
<Border Name="Bd"
|
||||
BorderBrush="{DynamicResource MaterialDesignDivider}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Height="25">
|
||||
<Grid Margin="{Binding Converter={StaticResource PropertyTreeMarginConverter}}" ColumnDefinitions="19,*">
|
||||
<ToggleButton x:Name="Expander"
|
||||
Foreground="{DynamicResource MaterialDesignBody}"
|
||||
IsChecked="{Binding Path=LayerPropertyGroupViewModel.IsExpanded}"
|
||||
IsVisible="{Binding LayerPropertyGroupViewModel.HasChildren}"
|
||||
ClickMode="Press" />
|
||||
|
||||
<StackPanel Grid.Column="1">
|
||||
<!-- Type: None -->
|
||||
<TextBlock Text="{Binding LayerPropertyGroup.GroupDescription.Name}"
|
||||
ToolTip.Tip="{Binding LayerPropertyGroup.GroupDescription.Description}"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="3 5 0 5"
|
||||
IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.None}}">
|
||||
</TextBlock>
|
||||
|
||||
<!-- Type: General -->
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Margin="0 5"
|
||||
IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.General}}">
|
||||
<avalonia:MaterialIcon Kind="HammerWrench" Margin="0 -1 5 0" />
|
||||
<TextBlock ToolTip.Tip="{Binding LayerPropertyGroup.GroupDescription.Description}">General</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Type: Transform -->
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Margin="0 5"
|
||||
IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.Transform}}">
|
||||
<avalonia:MaterialIcon Kind="TransitConnectionVariant" Margin="0 -1 5 0" />
|
||||
<TextBlock ToolTip.Tip="{Binding LayerPropertyGroup.GroupDescription.Description}">Transform</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Type: LayerBrushRoot -->
|
||||
<Grid IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.LayerBrushRoot}}"
|
||||
ColumnDefinitions="Auto,Auto,Auto,*">
|
||||
<controls:ArtemisIcon Grid.Column="0"
|
||||
Icon="{Binding LayerPropertyGroup.LayerBrush.Descriptor.Icon}"
|
||||
Width="16"
|
||||
Height="16"
|
||||
Margin="0 5 5 0" />
|
||||
<TextBlock Grid.Column="1"
|
||||
ToolTip.Tip="{Binding LayerPropertyGroup.LayerBrush.Descriptor.Description}"
|
||||
Margin="0 5 0 0">
|
||||
Brush - 
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="{Binding LayerPropertyGroup.LayerBrush.Descriptor.DisplayName}"
|
||||
ToolTip.Tip="{Binding LayerPropertyGroup.LayerBrush.Descriptor.Description}"
|
||||
Margin="0 5 0 0" />
|
||||
|
||||
<StackPanel Grid.Column="3"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
IsVisible="{Binding LayerPropertyGroup.LayerBrush.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<TextBlock VerticalAlignment="Center">Extra options available!</TextBlock>
|
||||
<avalonia:MaterialIcon Kind="ChevronRight" VerticalAlignment="Center">
|
||||
<avalonia:MaterialIcon.RenderTransform>
|
||||
<TranslateTransform X="0" />
|
||||
</avalonia:MaterialIcon.RenderTransform>
|
||||
</avalonia:MaterialIcon>
|
||||
<Button Classes="icon-button" ToolTip.Tip="Open brush settings" Width="24" Height="24" HorizontalAlignment="Right" Command="{Binding OpenBrushSettings}">
|
||||
<avalonia:MaterialIcon Kind="Settings" Height="16" Width="16" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Type: LayerEffectRoot -->
|
||||
<Grid Height="24"
|
||||
IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.LayerEffectRoot}}"
|
||||
ColumnDefinitions="Auto,Auto,Auto,Auto,*,Auto">
|
||||
<controls:ArtemisIcon
|
||||
Grid.Column="0"
|
||||
Cursor="SizeNorthSouth"
|
||||
Icon="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Icon}"
|
||||
Width="16"
|
||||
Height="16"
|
||||
Margin="0 5 5 0"
|
||||
Background="Transparent" />
|
||||
<TextBlock Grid.Column="1" ToolTip.Tip="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Description}" Margin="0 5 0 0">
|
||||
Effect
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Column="2"
|
||||
ToolTip.Tip="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Description}"
|
||||
Margin="3 5">
|
||||
-
|
||||
</TextBlock>
|
||||
|
||||
<!-- Show either the descriptors display name or, if set, the effect name -->
|
||||
<TextBlock Grid.Column="3"
|
||||
Text="{Binding LayerPropertyGroup.LayerEffect.Descriptor.DisplayName}"
|
||||
ToolTip.Tip="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Description}"
|
||||
Margin="0 5"
|
||||
IsVisible="{Binding !LayerPropertyGroup.LayerEffect.Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
<TextBlock Grid.Column="4"
|
||||
Text="{Binding LayerPropertyGroup.LayerEffect.Name}"
|
||||
ToolTip.Tip="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Description}"
|
||||
Margin="0 5"
|
||||
IsVisible="{Binding LayerPropertyGroup.LayerEffect.Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
|
||||
<StackPanel Grid.Column="5" Orientation="Horizontal">
|
||||
<ToggleButton
|
||||
Classes="icon-button"
|
||||
ToolTip.Tip="Toggle suspended state"
|
||||
Width="18"
|
||||
Height="18"
|
||||
IsChecked="{Binding !LayerPropertyGroup.LayerEffect.Suspended}"
|
||||
VerticalAlignment="Center" Padding="-25"
|
||||
Margin="5 0"
|
||||
Command="{Binding SuspendedToggled}">
|
||||
<avalonia:MaterialIcon Kind="Eye" Height="13" Width="13" />
|
||||
</ToggleButton>
|
||||
<Button Classes="icon-button"
|
||||
ToolTip.Tip="Rename"
|
||||
Width="24"
|
||||
Height="24"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding RenameEffect}">
|
||||
<avalonia:MaterialIcon Kind="RenameBox" Height="16" Width="16" />
|
||||
</Button>
|
||||
<Button Classes="icon-button"
|
||||
ToolTip.Tip="Open effect settings"
|
||||
Width="24"
|
||||
Height="24"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding OpenEffectSettings}"
|
||||
IsVisible="{Binding LayerPropertyGroup.LayerEffect.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<avalonia:MaterialIcon Kind="Settings" Height="16" Width="16" />
|
||||
</Button>
|
||||
<Button Classes="icon-button"
|
||||
ToolTip.Tip="Remove"
|
||||
Width="24"
|
||||
Height="24"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding DeleteEffect}">
|
||||
<avalonia:MaterialIcon Kind="TrashCan" Height="16" Width="16" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
Do not bind directly to the LayerPropertyGroupViewModel.Children collection
|
||||
Instead use a reference provided by the VM that is null when collapsed, virtualization for noobs
|
||||
-->
|
||||
<ItemsControl Items="{Binding Children}"
|
||||
IsVisible="{Binding LayerPropertyGroupViewModel.IsExpanded}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ItemsControl.DataTemplates>
|
||||
<DataTemplate DataType="profileElementProperties:ProfileElementPropertyGroupViewModel">
|
||||
<ContentControl Content="{Binding TreeGroupViewModel}" IsVisible="{Binding IsVisible}"></ContentControl>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="profileElementProperties:ProfileElementPropertyViewModel">
|
||||
<ContentControl Content="{Binding TreePropertyViewModel}" IsVisible="{Binding IsVisible}"></ContentControl>
|
||||
</DataTemplate>
|
||||
</ItemsControl.DataTemplates>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -0,0 +1,18 @@
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree
|
||||
{
|
||||
public partial class TreeGroupView : ReactiveUserControl<TreeGroupViewModel>
|
||||
{
|
||||
public TreeGroupView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.LayerBrushes;
|
||||
using Artemis.Core.LayerEffects;
|
||||
using Artemis.UI.Exceptions;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.LayerBrushes;
|
||||
using Artemis.UI.Shared.LayerEffects;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Ninject;
|
||||
using Ninject.Parameters;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||
|
||||
public class TreeGroupViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly IWindowService _windowService;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private BrushConfigurationWindowViewModel? _brushConfigurationWindowViewModel;
|
||||
private EffectConfigurationWindowViewModel? _effectConfigurationWindowViewModel;
|
||||
|
||||
public TreeGroupViewModel(ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel, IWindowService windowService, IProfileEditorService profileEditorService)
|
||||
{
|
||||
_windowService = windowService;
|
||||
_profileEditorService = profileEditorService;
|
||||
ProfileElementPropertyGroupViewModel = profileElementPropertyGroupViewModel;
|
||||
DetermineGroupType();
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
ProfileElementPropertyGroupViewModel.WhenAnyValue(vm => vm.IsExpanded).Subscribe(_ => this.RaisePropertyChanged(nameof(Children))).DisposeWith(d);
|
||||
Disposable.Create(CloseViewModels).DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel { get; }
|
||||
public LayerPropertyGroup LayerPropertyGroup => ProfileElementPropertyGroupViewModel.LayerPropertyGroup;
|
||||
public ObservableCollection<ActivatableViewModelBase>? Children => ProfileElementPropertyGroupViewModel.IsExpanded ? ProfileElementPropertyGroupViewModel.Children : null;
|
||||
|
||||
public LayerPropertyGroupType GroupType { get; private set; }
|
||||
|
||||
public async Task OpenBrushSettings()
|
||||
{
|
||||
BaseLayerBrush? layerBrush = LayerPropertyGroup.LayerBrush;
|
||||
if (layerBrush?.ConfigurationDialog is not LayerBrushConfigurationDialog configurationViewModel)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// Limit to one constructor, there's no need to have more and it complicates things anyway
|
||||
ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors();
|
||||
if (constructors.Length != 1)
|
||||
throw new ArtemisUIException("Brush configuration dialogs must have exactly one constructor");
|
||||
|
||||
// Find the BaseLayerBrush parameter, it is required by the base constructor so its there for sure
|
||||
ParameterInfo brushParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerBrush).IsAssignableFrom(p.ParameterType));
|
||||
ConstructorArgument argument = new(brushParameter.Name!, layerBrush);
|
||||
BrushConfigurationViewModel viewModel = (BrushConfigurationViewModel) layerBrush.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument);
|
||||
|
||||
_brushConfigurationWindowViewModel = new BrushConfigurationWindowViewModel(viewModel, configurationViewModel);
|
||||
await _windowService.ShowDialogAsync(_brushConfigurationWindowViewModel);
|
||||
|
||||
// Save changes after the dialog closes
|
||||
await _profileEditorService.SaveProfileAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_windowService.ShowExceptionDialog("An exception occurred while trying to show the brush's settings window", e);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OpenEffectSettings()
|
||||
{
|
||||
BaseLayerEffect? layerEffect = LayerPropertyGroup.LayerEffect;
|
||||
if (layerEffect?.ConfigurationDialog is not LayerEffectConfigurationDialog configurationViewModel)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// Limit to one constructor, there's no need to have more and it complicates things anyway
|
||||
ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors();
|
||||
if (constructors.Length != 1)
|
||||
throw new ArtemisUIException("Effect configuration dialogs must have exactly one constructor");
|
||||
|
||||
// Find the BaseLayerEffect parameter, it is required by the base constructor so its there for sure
|
||||
ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType));
|
||||
ConstructorArgument argument = new(effectParameter.Name!, layerEffect);
|
||||
EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel)layerEffect.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument);
|
||||
|
||||
_effectConfigurationWindowViewModel = new EffectConfigurationWindowViewModel(viewModel, configurationViewModel);
|
||||
await _windowService.ShowDialogAsync(_effectConfigurationWindowViewModel);
|
||||
|
||||
// Save changes after the dialog closes
|
||||
await _profileEditorService.SaveProfileAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_windowService.ShowExceptionDialog("An exception occurred while trying to show the effect's settings window", e);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RenameEffect()
|
||||
{
|
||||
await _windowService.ShowConfirmContentDialog("Not yet implemented", "Try again later :p");
|
||||
}
|
||||
|
||||
public async Task DeleteEffect()
|
||||
{
|
||||
await _windowService.ShowConfirmContentDialog("Not yet implemented", "Try again later :p");
|
||||
}
|
||||
|
||||
public double GetDepth()
|
||||
{
|
||||
int depth = 0;
|
||||
LayerPropertyGroup? current = LayerPropertyGroup.Parent;
|
||||
while (current != null)
|
||||
{
|
||||
depth++;
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
return depth;
|
||||
}
|
||||
|
||||
private void CloseViewModels()
|
||||
{
|
||||
_effectConfigurationWindowViewModel?.Close(null);
|
||||
_brushConfigurationWindowViewModel?.Close(null);
|
||||
}
|
||||
|
||||
private void DetermineGroupType()
|
||||
{
|
||||
if (LayerPropertyGroup is LayerGeneralProperties)
|
||||
GroupType = LayerPropertyGroupType.General;
|
||||
else if (LayerPropertyGroup is LayerTransformProperties)
|
||||
GroupType = LayerPropertyGroupType.Transform;
|
||||
else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerBrush != null)
|
||||
GroupType = LayerPropertyGroupType.LayerBrushRoot;
|
||||
else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerEffect != null)
|
||||
GroupType = LayerPropertyGroupType.LayerEffectRoot;
|
||||
else
|
||||
GroupType = LayerPropertyGroupType.None;
|
||||
}
|
||||
}
|
||||
|
||||
public enum LayerPropertyGroupType
|
||||
{
|
||||
General,
|
||||
Transform,
|
||||
LayerBrushRoot,
|
||||
LayerEffectRoot,
|
||||
None
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree.TreePropertyView">
|
||||
Welcome to Avalonia!
|
||||
</UserControl>
|
||||
@ -0,0 +1,19 @@
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree
|
||||
{
|
||||
public partial class TreePropertyView : ReactiveUserControl<IActivatableViewModel>
|
||||
{
|
||||
public TreePropertyView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.PropertyInput;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||
|
||||
internal class TreePropertyViewModel<T> : ActivatableViewModelBase
|
||||
{
|
||||
public TreePropertyViewModel(LayerProperty<T> layerProperty, ProfileElementPropertyViewModel layerPropertyViewModel, IPropertyInputService propertyInputService)
|
||||
{
|
||||
LayerProperty = layerProperty;
|
||||
LayerPropertyViewModel = layerPropertyViewModel;
|
||||
PropertyInputViewModel = propertyInputService.CreatePropertyInputViewModel(LayerProperty);
|
||||
}
|
||||
|
||||
public LayerProperty<T> LayerProperty { get; }
|
||||
public ProfileElementPropertyViewModel LayerPropertyViewModel { get; }
|
||||
public PropertyInputViewModel<T>? PropertyInputViewModel { get; }
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
<controls:CoreWindow xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="800"
|
||||
d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows.BrushConfigurationWindowView"
|
||||
Title="Artemis | Brush configuration"
|
||||
Width="{Binding Configuration.DialogWidth}"
|
||||
Height="{Binding Configuration.DialogHeight}">
|
||||
<Panel>
|
||||
<ContentControl Content="{Binding ConfigurationViewModel}"></ContentControl>
|
||||
</Panel>
|
||||
</controls:CoreWindow>
|
||||
@ -0,0 +1,27 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows;
|
||||
|
||||
public class BrushConfigurationWindowView : ReactiveCoreWindow<EffectConfigurationWindowViewModel>
|
||||
{
|
||||
public BrushConfigurationWindowView()
|
||||
{
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
Closing += OnClosing;
|
||||
}
|
||||
|
||||
private void OnClosing(object? sender, CancelEventArgs e)
|
||||
{
|
||||
e.Cancel = ViewModel?.CanClose() ?? true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.LayerBrushes;
|
||||
using Artemis.UI.Shared.LayerEffects;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows;
|
||||
|
||||
public class BrushConfigurationWindowViewModel : DialogViewModelBase<object?>
|
||||
{
|
||||
public BrushConfigurationWindowViewModel(BrushConfigurationViewModel configurationViewModel, LayerBrushConfigurationDialog configuration)
|
||||
{
|
||||
ConfigurationViewModel = configurationViewModel;
|
||||
Configuration = configuration;
|
||||
|
||||
ConfigurationViewModel.CloseRequested += ConfigurationViewModelOnCloseRequested;
|
||||
}
|
||||
|
||||
public BrushConfigurationViewModel ConfigurationViewModel { get; }
|
||||
public LayerBrushConfigurationDialog Configuration { get; }
|
||||
|
||||
public bool CanClose()
|
||||
{
|
||||
return ConfigurationViewModel.CanClose() && Dispatcher.UIThread.InvokeAsync(async () => await ConfigurationViewModel.CanCloseAsync()).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private void ConfigurationViewModelOnCloseRequested(object? sender, EventArgs e)
|
||||
{
|
||||
if (CanClose())
|
||||
Close(null);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
<controls:CoreWindow xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="800"
|
||||
d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows.EffectConfigurationWindowView"
|
||||
Title="Artemis | Effect configuration"
|
||||
Width="{Binding Configuration.DialogWidth}"
|
||||
Height="{Binding Configuration.DialogHeight}">
|
||||
<Panel>
|
||||
<ContentControl Content="{Binding ConfigurationViewModel}"></ContentControl>
|
||||
</Panel>
|
||||
</controls:CoreWindow>
|
||||
@ -0,0 +1,27 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows;
|
||||
|
||||
public class EffectConfigurationWindowView : ReactiveCoreWindow<EffectConfigurationWindowViewModel>
|
||||
{
|
||||
public EffectConfigurationWindowView()
|
||||
{
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
Closing += OnClosing;
|
||||
}
|
||||
|
||||
private void OnClosing(object? sender, CancelEventArgs e)
|
||||
{
|
||||
e.Cancel = ViewModel?.CanClose() ?? true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.LayerEffects;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows;
|
||||
|
||||
public class EffectConfigurationWindowViewModel : DialogViewModelBase<object?>
|
||||
{
|
||||
public EffectConfigurationWindowViewModel(EffectConfigurationViewModel configurationViewModel, LayerEffectConfigurationDialog configuration)
|
||||
{
|
||||
ConfigurationViewModel = configurationViewModel;
|
||||
Configuration = configuration;
|
||||
|
||||
ConfigurationViewModel.CloseRequested += ConfigurationViewModelOnCloseRequested;
|
||||
}
|
||||
|
||||
public EffectConfigurationViewModel ConfigurationViewModel { get; }
|
||||
public LayerEffectConfigurationDialog Configuration { get; }
|
||||
|
||||
public bool CanClose()
|
||||
{
|
||||
return ConfigurationViewModel.CanClose() && Dispatcher.UIThread.InvokeAsync(async () => await ConfigurationViewModel.CanCloseAsync()).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private void ConfigurationViewModelOnCloseRequested(object? sender, EventArgs e)
|
||||
{
|
||||
if (CanClose())
|
||||
Close(null);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
|
||||
{
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
|
||||
{
|
||||
|
||||
@ -5,8 +5,8 @@ using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
|
||||
|
||||
@ -8,10 +8,10 @@ using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.ProfileEditor.Commands;
|
||||
using Artemis.UI.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
|
||||
|
||||
@ -3,8 +3,8 @@ using System.Collections.ObjectModel;
|
||||
using System.Reactive.Disposables;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.VisualEditor
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using Artemis.UI.Screens.ProfileEditor.Panels.MenuBar;
|
||||
using Artemis.UI.Screens.ProfileEditor.MenuBar;
|
||||
using Artemis.UI.Services.Interfaces;
|
||||
using Artemis.UI.Shared;
|
||||
|
||||
|
||||
@ -63,7 +63,7 @@
|
||||
<GridSplitter Grid.Row="1" Classes="editor-grid-splitter-horizontal" />
|
||||
|
||||
<Border Grid.Row="2" Classes="card card-condensed" Margin="4">
|
||||
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center">Properties/timeline</TextBlock>
|
||||
<ContentControl Content="{Binding ProfileElementPropertiesViewModel}"></ContentControl>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Screens.ProfileEditor.Panels.MenuBar;
|
||||
using Artemis.UI.Screens.ProfileEditor.MenuBar;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileTree;
|
||||
using Artemis.UI.Screens.ProfileEditor.VisualEditor;
|
||||
using Artemis.UI.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Ninject;
|
||||
using ReactiveUI;
|
||||
|
||||
@ -12,8 +13,8 @@ namespace Artemis.UI.Screens.ProfileEditor
|
||||
{
|
||||
public class ProfileEditorViewModel : MainScreenViewModel
|
||||
{
|
||||
private ProfileConfiguration? _profileConfiguration;
|
||||
private ProfileEditorHistory? _history;
|
||||
private ObservableAsPropertyHelper<ProfileConfiguration?>? _profileConfiguration;
|
||||
private ObservableAsPropertyHelper<ProfileEditorHistory?>? _history;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ProfileEditorViewModel(IScreen hostScreen,
|
||||
@ -22,37 +23,30 @@ namespace Artemis.UI.Screens.ProfileEditor
|
||||
VisualEditorViewModel visualEditorViewModel,
|
||||
ProfileTreeViewModel profileTreeViewModel,
|
||||
ProfileEditorTitleBarViewModel profileEditorTitleBarViewModel,
|
||||
MenuBarViewModel menuBarViewModel)
|
||||
MenuBarViewModel menuBarViewModel,
|
||||
ProfileElementPropertiesViewModel profileElementPropertiesViewModel)
|
||||
: base(hostScreen, "profile-editor")
|
||||
{
|
||||
VisualEditorViewModel = visualEditorViewModel;
|
||||
ProfileTreeViewModel = profileTreeViewModel;
|
||||
ProfileElementPropertiesViewModel = profileElementPropertiesViewModel;
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
TitleBarViewModel = profileEditorTitleBarViewModel;
|
||||
else
|
||||
MenuBarViewModel = menuBarViewModel;
|
||||
|
||||
|
||||
this.WhenActivated(d => profileEditorService.ProfileConfiguration.WhereNotNull().Subscribe(p => ProfileConfiguration = p).DisposeWith(d));
|
||||
this.WhenActivated(d => profileEditorService.History.Subscribe(history => History = history).DisposeWith(d));
|
||||
this.WhenActivated(d => _profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d));
|
||||
this.WhenActivated(d => _history = profileEditorService.History.ToProperty(this, vm => vm.History).DisposeWith(d));
|
||||
}
|
||||
|
||||
public VisualEditorViewModel VisualEditorViewModel { get; }
|
||||
public ProfileTreeViewModel ProfileTreeViewModel { get; }
|
||||
public MenuBarViewModel? MenuBarViewModel { get; }
|
||||
public ProfileElementPropertiesViewModel ProfileElementPropertiesViewModel { get; }
|
||||
|
||||
public ProfileConfiguration? ProfileConfiguration
|
||||
{
|
||||
get => _profileConfiguration;
|
||||
set => this.RaiseAndSetIfChanged(ref _profileConfiguration, value);
|
||||
}
|
||||
|
||||
public ProfileEditorHistory? History
|
||||
{
|
||||
get => _history;
|
||||
set => this.RaiseAndSetIfChanged(ref _history, value);
|
||||
}
|
||||
public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value;
|
||||
public ProfileEditorHistory? History => _history?.Value;
|
||||
|
||||
public void OpenUrl(string url)
|
||||
{
|
||||
|
||||
@ -9,7 +9,7 @@ using Artemis.UI.Screens.Sidebar;
|
||||
using Artemis.UI.Services.Interfaces;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services.MainWindowService;
|
||||
using Artemis.UI.Shared.Services.MainWindow;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
|
||||
@ -6,10 +6,10 @@ using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.Builders;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Sidebar
|
||||
|
||||
@ -11,10 +11,10 @@ using Artemis.UI.Screens.ProfileEditor;
|
||||
using Artemis.UI.Screens.Settings;
|
||||
using Artemis.UI.Screens.SurfaceEditor;
|
||||
using Artemis.UI.Screens.Workshop;
|
||||
using Artemis.UI.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.Builders;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Material.Icons;
|
||||
using Ninject;
|
||||
using ReactiveUI;
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Exceptions;
|
||||
|
||||
namespace Artemis.UI.Services.ProfileEditor
|
||||
{
|
||||
public class ProfileEditorService : IProfileEditorService
|
||||
{
|
||||
private readonly Dictionary<ProfileConfiguration, ProfileEditorHistory> _profileEditorHistories = new();
|
||||
private readonly BehaviorSubject<ProfileConfiguration?> _profileConfigurationSubject = new(null);
|
||||
private readonly BehaviorSubject<RenderProfileElement?> _profileElementSubject = new(null);
|
||||
|
||||
public ProfileEditorService()
|
||||
{
|
||||
ProfileConfiguration = _profileConfigurationSubject.AsObservable().DistinctUntilChanged();
|
||||
ProfileElement = _profileElementSubject.AsObservable().DistinctUntilChanged();
|
||||
History = Observable.Defer(() => Observable.Return(GetHistory(_profileConfigurationSubject.Value))).Concat(ProfileConfiguration.Select(GetHistory));
|
||||
}
|
||||
|
||||
private ProfileEditorHistory? GetHistory(ProfileConfiguration? profileConfiguration)
|
||||
{
|
||||
if (profileConfiguration == null)
|
||||
return null;
|
||||
if (_profileEditorHistories.TryGetValue(profileConfiguration, out ProfileEditorHistory? history))
|
||||
return history;
|
||||
|
||||
ProfileEditorHistory newHistory = new(profileConfiguration);
|
||||
_profileEditorHistories.Add(profileConfiguration, newHistory);
|
||||
return newHistory;
|
||||
}
|
||||
|
||||
public IObservable<ProfileConfiguration?> ProfileConfiguration { get; }
|
||||
public IObservable<RenderProfileElement?> ProfileElement { get; }
|
||||
public IObservable<ProfileEditorHistory?> History { get; }
|
||||
|
||||
public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration)
|
||||
{
|
||||
_profileConfigurationSubject.OnNext(profileConfiguration);
|
||||
}
|
||||
|
||||
public void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement)
|
||||
{
|
||||
_profileElementSubject.OnNext(renderProfileElement);
|
||||
}
|
||||
|
||||
public void ExecuteCommand(IProfileEditorCommand command)
|
||||
{
|
||||
ProfileEditorHistory? history = GetHistory(_profileConfigurationSubject.Value);
|
||||
if (history == null)
|
||||
throw new ArtemisUIException("Can't execute a command when there's no active profile configuration");
|
||||
|
||||
history.Execute.Execute(command).Subscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user