1
0
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:
Robert 2022-01-10 00:07:06 +01:00
parent 126540e2f3
commit 0b905cca2e
62 changed files with 1807 additions and 1805 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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; }
}
}

View File

@ -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;
}

View File

@ -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; }
}
}

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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.

View File

@ -1,6 +1,6 @@
using System;
namespace Artemis.UI.Shared.Services.MainWindowService
namespace Artemis.UI.Shared.Services.MainWindow
{
internal class MainWindowService : IMainWindowService
{

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -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
{

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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">

View File

@ -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>
{

View File

@ -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
{

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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; }
}

View File

@ -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 -&#160;
</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>

View File

@ -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);
}
}
}

View File

@ -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
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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; }
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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
{

View File

@ -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
{

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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>

View File

@ -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)
{

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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();
}
}
}