1
0

feat: add pref window view model

This commit is contained in:
2026-04-20 10:45:16 +08:00
parent 6cb984fa92
commit 394768d862
8 changed files with 208 additions and 65 deletions

View File

@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace BallanceTasEditor.Frontend.Converters {
internal class GenericEnumRadioButtonConverter<T> where T: struct {
public object Convert(object value, object parameter) {
var susValue = value as T?;
var susParam = parameter as T?;
if (susValue is null || susParam is null) {
return Binding.DoNothing;
} else {
return susValue.Value.Equals(susParam.Value);
}
}
public object ConvertBack(object value, object parameter) {
var susValue = value as bool?;
var susParam = parameter as T?;
if (susValue is null || susParam is null) {
return Binding.DoNothing;
} else {
return susValue.Value ? susParam.Value : Binding.DoNothing;
}
}
}
[ValueConversion(typeof(Shared.SequenceKind), typeof(bool))]
public class SequenceKindRadioButtonConverter : IValueConverter {
public static readonly SequenceKindRadioButtonConverter Instance = new();
private readonly GenericEnumRadioButtonConverter<Shared.SequenceKind> m_Inner = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return m_Inner.Convert(value, parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return m_Inner.ConvertBack(value, parameter);
}
}
[ValueConversion(typeof(Shared.EditorLayoutKind), typeof(bool))]
public class EditorLayoutKindRadioButtonConverter : IValueConverter {
public static readonly EditorLayoutKindRadioButtonConverter Instance = new();
private readonly GenericEnumRadioButtonConverter<Shared.EditorLayoutKind> m_Inner = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return m_Inner.Convert(value, parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return m_Inner.ConvertBack(value, parameter);
}
}
[ValueConversion(typeof(Shared.EditorPasteBehavior), typeof(bool))]
public class EditorPasteBehaviorRadioButtonConverter : IValueConverter {
public static readonly EditorPasteBehaviorRadioButtonConverter Instance = new();
private readonly GenericEnumRadioButtonConverter<Shared.EditorPasteBehavior> m_Inner = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return m_Inner.Convert(value, parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return m_Inner.ConvertBack(value, parameter);
}
}
}

View File

@@ -23,11 +23,11 @@ namespace BallanceTasEditor.Frontend.Models {
}
[ObservableProperty]
private Shared.TasSequenceKind sequenceKind;
private Shared.SequenceKind sequenceKind;
[ObservableProperty]
private Shared.EditorLayoutKind editorLayout;
[ObservableProperty]
private Shared.EditorPasteMode pasteMode;
private Shared.EditorPasteBehavior pasteBehavior;
[ObservableProperty]
private int frameCount;
[ObservableProperty]
@@ -35,7 +35,7 @@ namespace BallanceTasEditor.Frontend.Models {
[MemberNotNull(nameof(SequenceKind))]
[MemberNotNull(nameof(EditorLayout))]
[MemberNotNull(nameof(PasteMode))]
[MemberNotNull(nameof(PasteBehavior))]
[MemberNotNull(nameof(FrameCount))]
[MemberNotNull(nameof(GamePath))]
private void FromSingleton() {
@@ -43,7 +43,7 @@ namespace BallanceTasEditor.Frontend.Models {
SequenceKind = singleton.SequenceKind;
EditorLayout = singleton.EditorLayout;
PasteMode = singleton.PasteMode;
PasteBehavior = singleton.PasteBehavior;
FrameCount = singleton.FrameCount;
GamePath = singleton.GamePath;
}
@@ -53,7 +53,7 @@ namespace BallanceTasEditor.Frontend.Models {
singleton.SequenceKind = SequenceKind;
singleton.EditorLayout = EditorLayout;
singleton.PasteMode = PasteMode;
singleton.PasteBehavior = PasteBehavior;
singleton.FrameCount = FrameCount;
singleton.GamePath = GamePath;
}

View File

@@ -30,28 +30,28 @@ namespace BallanceTasEditor.Frontend.Models {
get => FileBody is null;
}
public void NewFile(Shared.TasSequenceKind kind, int count, uint fps) {
public void NewFile(Shared.SequenceKind kind, int count, uint fps) {
// Check status
if (IsFileLoaded) {
throw new InvalidOperationException();
}
// Initialize sequence
var seq = Shared.TasSequenceKindHelper.CreateSequenceByKind(kind);
var seq = Shared.SequenceKindHelper.CreateSequenceByKind(kind);
// Initialize items
Backend.TasStorage.Init(seq, count, fps);
// Set members
FileBody = seq;
}
public void LoadFile(Shared.TasSequenceKind kind, string path) {
public void LoadFile(Shared.SequenceKind kind, string path) {
// Check status
if (IsFileLoaded) {
throw new InvalidOperationException();
}
// Initialize sequence
var seq = Shared.TasSequenceKindHelper.CreateSequenceByKind(kind);
var seq = Shared.SequenceKindHelper.CreateSequenceByKind(kind);
// Load into sequence
Backend.TasStorage.Load(path, seq);
// Set members

View File

@@ -18,21 +18,21 @@ namespace BallanceTasEditor.Frontend.Shared {
Init();
}
public TasSequenceKind SequenceKind { get; set; }
public SequenceKind SequenceKind { get; set; }
public EditorLayoutKind EditorLayout { get; set; }
public EditorPasteMode PasteMode { get; set; }
public EditorPasteBehavior PasteBehavior { get; set; }
public int FrameCount { get; set; }
public string GamePath { get; set; }
[MemberNotNull(nameof(SequenceKind))]
[MemberNotNull(nameof(EditorLayout))]
[MemberNotNull(nameof(PasteMode))]
[MemberNotNull(nameof(PasteBehavior))]
[MemberNotNull(nameof(FrameCount))]
[MemberNotNull(nameof(GamePath))]
private void FromRaw(RawEditorConfiguration raw) {
SequenceKind = raw.SequenceKind;
EditorLayout = raw.EditorLayout;
PasteMode = raw.PasteMode;
PasteBehavior = raw.PasteBehavior;
FrameCount = raw.FrameCount;
GamePath = raw.GamePath;
}
@@ -41,7 +41,7 @@ namespace BallanceTasEditor.Frontend.Shared {
return new RawEditorConfiguration {
SequenceKind = SequenceKind,
EditorLayout = EditorLayout,
PasteMode = PasteMode,
PasteBehavior = PasteBehavior,
FrameCount = FrameCount,
GamePath = GamePath
};
@@ -69,7 +69,7 @@ namespace BallanceTasEditor.Frontend.Shared {
[MemberNotNull(nameof(SequenceKind))]
[MemberNotNull(nameof(EditorLayout))]
[MemberNotNull(nameof(PasteMode))]
[MemberNotNull(nameof(PasteBehavior))]
[MemberNotNull(nameof(FrameCount))]
[MemberNotNull(nameof(GamePath))]
private void Init() {
@@ -103,15 +103,15 @@ namespace BallanceTasEditor.Frontend.Shared {
private class RawEditorConfiguration {
[JsonProperty("sequence_kind", Required = Required.Always)]
[JsonConverter(typeof(TasSequenceKindConverter))]
public TasSequenceKind SequenceKind { get; set; } = TasSequenceKind.Array;
public SequenceKind SequenceKind { get; set; } = SequenceKind.Array;
[JsonProperty("editor_layout", Required = Required.Always)]
[JsonConverter(typeof(EditorLayoutKindConverter))]
public EditorLayoutKind EditorLayout { get; set; } = EditorLayoutKind.Vertical;
[JsonProperty("paste_mode", Required = Required.Always)]
[JsonConverter(typeof(EditorPasteModeConverter))]
public EditorPasteMode PasteMode { get; set; } = EditorPasteMode.Insert;
[JsonProperty("paste_behavior", Required = Required.Always)]
[JsonConverter(typeof(EditorPasteBehaviorConverter))]
public EditorPasteBehavior PasteBehavior { get; set; } = EditorPasteBehavior.Insert;
[JsonProperty("frame_count", Required = Required.Always)]
public int FrameCount { get; set; } = 20;
@@ -122,11 +122,11 @@ namespace BallanceTasEditor.Frontend.Shared {
#region Custom JSON Converter
private class TasSequenceKindConverter : JsonConverter<TasSequenceKind> {
public override TasSequenceKind ReadJson(JsonReader reader, Type objectType, TasSequenceKind existingValue, bool hasExistingValue, JsonSerializer serializer) {
private class TasSequenceKindConverter : JsonConverter<SequenceKind> {
public override SequenceKind ReadJson(JsonReader reader, Type objectType, SequenceKind existingValue, bool hasExistingValue, JsonSerializer serializer) {
if (reader.TokenType == JsonToken.String) {
var value = (reader.Value as string).Unwrap();
if (TasSequenceKindHelper.TryParse(value, out var kind)) {
if (SequenceKindHelper.TryParse(value, out var kind)) {
return kind;
} else {
throw new JsonSerializationException($"given string can not be parsed as TasSequenceKind: {value}");
@@ -136,8 +136,8 @@ namespace BallanceTasEditor.Frontend.Shared {
}
}
public override void WriteJson(JsonWriter writer, TasSequenceKind value, JsonSerializer serializer) {
writer.WriteValue(TasSequenceKindHelper.ToString(value));
public override void WriteJson(JsonWriter writer, SequenceKind value, JsonSerializer serializer) {
writer.WriteValue(SequenceKindHelper.ToString(value));
}
}
@@ -160,22 +160,22 @@ namespace BallanceTasEditor.Frontend.Shared {
}
}
private class EditorPasteModeConverter : JsonConverter<EditorPasteMode> {
public override EditorPasteMode ReadJson(JsonReader reader, Type objectType, EditorPasteMode existingValue, bool hasExistingValue, JsonSerializer serializer) {
private class EditorPasteBehaviorConverter : JsonConverter<EditorPasteBehavior> {
public override EditorPasteBehavior ReadJson(JsonReader reader, Type objectType, EditorPasteBehavior existingValue, bool hasExistingValue, JsonSerializer serializer) {
if (reader.TokenType == JsonToken.String) {
var value = (reader.Value as string).Unwrap();
if (EditorPasteModeHelper.TryParse(value, out var kind)) {
if (EditorPasteBehaviorHelper.TryParse(value, out var kind)) {
return kind;
} else {
throw new JsonSerializationException($"given string can not be parsed as EditorPasteMode: {value}");
throw new JsonSerializationException($"given string can not be parsed as EditorPasteBehavior: {value}");
}
} else {
throw new JsonSerializationException($"expect a integer but got {reader.TokenType}");
}
}
public override void WriteJson(JsonWriter writer, EditorPasteMode value, JsonSerializer serializer) {
writer.WriteValue(EditorPasteModeHelper.ToString(value));
public override void WriteJson(JsonWriter writer, EditorPasteBehavior value, JsonSerializer serializer) {
writer.WriteValue(EditorPasteBehaviorHelper.ToString(value));
}
}

View File

@@ -8,27 +8,27 @@ using System.Threading.Tasks;
namespace BallanceTasEditor.Frontend.Shared {
public enum TasSequenceKind {
public enum SequenceKind {
Array,
DoubleLinkedList
}
public static class TasSequenceKindHelper {
public static Backend.ITasSequence CreateSequenceByKind(TasSequenceKind kind) {
public static class SequenceKindHelper {
public static Backend.ITasSequence CreateSequenceByKind(SequenceKind kind) {
return kind switch {
TasSequenceKind.Array => new Backend.ListTasSequence(),
TasSequenceKind.DoubleLinkedList => new Backend.LegacyTasSequence(),
SequenceKind.Array => new Backend.ListTasSequence(),
SequenceKind.DoubleLinkedList => new Backend.LegacyTasSequence(),
_ => throw new UnreachableException(),
};
}
public static bool TryParse(string s, out TasSequenceKind value) {
public static bool TryParse(string s, out SequenceKind value) {
switch (s) {
case "array":
value = TasSequenceKind.Array;
value = SequenceKind.Array;
return true;
case "double_linked_list":
value = TasSequenceKind.DoubleLinkedList;
value = SequenceKind.DoubleLinkedList;
return true;
default:
value = default;
@@ -36,10 +36,10 @@ namespace BallanceTasEditor.Frontend.Shared {
}
}
public static string ToString(TasSequenceKind kind) {
public static string ToString(SequenceKind kind) {
return kind switch {
TasSequenceKind.Array => "array",
TasSequenceKind.DoubleLinkedList => "double_linked_list",
SequenceKind.Array => "array",
SequenceKind.DoubleLinkedList => "double_linked_list",
_ => throw new UnreachableException(),
};
}
@@ -73,19 +73,19 @@ namespace BallanceTasEditor.Frontend.Shared {
}
}
public enum EditorPasteMode {
public enum EditorPasteBehavior {
Insert,
Override
}
public static class EditorPasteModeHelper {
public static bool TryParse(string s, out EditorPasteMode value) {
public static class EditorPasteBehaviorHelper {
public static bool TryParse(string s, out EditorPasteBehavior value) {
switch (s) {
case "insert":
value = EditorPasteMode.Insert;
value = EditorPasteBehavior.Insert;
return true;
case "override":
value = EditorPasteMode.Override;
value = EditorPasteBehavior.Override;
return true;
default:
value = default;
@@ -93,10 +93,10 @@ namespace BallanceTasEditor.Frontend.Shared {
}
}
public static string ToString(EditorPasteMode mode) {
public static string ToString(EditorPasteBehavior mode) {
return mode switch {
EditorPasteMode.Insert => "insert",
EditorPasteMode.Override => "override",
EditorPasteBehavior.Insert => "insert",
EditorPasteBehavior.Override => "override",
_ => throw new UnreachableException(),
};
}

View File

@@ -61,7 +61,7 @@ namespace BallanceTasEditor.Frontend.ViewModels {
if (dialog is null) return;
// Create new file
TasFile.NewFile(Shared.TasSequenceKind.Array, dialog.Count, dialog.Fps);
TasFile.NewFile(Shared.SequenceKind.Array, dialog.Count, dialog.Fps);
// Set members
TasFilePath = null;
// Send notification
@@ -80,7 +80,7 @@ namespace BallanceTasEditor.Frontend.ViewModels {
// Load file
try {
TasFile.LoadFile(Shared.TasSequenceKind.Array, dialog.Path);
TasFile.LoadFile(Shared.SequenceKind.Array, dialog.Path);
} catch (Exception e) {
m_DialogService.ShowOpenFileFailedDialog(e);
return;

View File

@@ -2,17 +2,47 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BallanceTasEditor.Frontend.ViewModels {
public partial class PreferenceDialog : ObservableObject {
public partial class PreferenceDialog : ObservableValidator {
public PreferenceDialog() {
}
[ObservableProperty]
private Shared.SequenceKind sequenceKind;
[ObservableProperty]
private Shared.EditorLayoutKind layoutKind;
[ObservableProperty]
private Shared.EditorPasteBehavior pasteBehavior;
[ObservableProperty]
[NotifyDataErrorInfo]
[CustomValidation(typeof(PreferenceDialog), nameof(ValidateCount))]
[NotifyCanExecuteChangedFor(nameof(OkCommand))]
private string frameCount;
[ObservableProperty]
private string gamePath;
#region Validators
private static readonly Validators.ValidatorAdapter<string, int, Validators.CountValidator> g_CountValidator =
new Validators.ValidatorAdapter<string, int, Validators.CountValidator>(new Validators.CountValidator());
public static ValidationResult? ValidateCount(string value, ValidationContext context) {
return g_CountValidator.Validate(value, context);
}
//public Shared.NewFileDialogResult GetUserInput() {
// return
//}
#endregion
#region Commands
[RelayCommand(CanExecute = nameof(CanOk))]
private void Ok() {
@@ -20,8 +50,7 @@ namespace BallanceTasEditor.Frontend.ViewModels {
}
private bool CanOk() {
// TODO
return true;
return !HasErrors;
}
[RelayCommand]
@@ -29,12 +58,13 @@ namespace BallanceTasEditor.Frontend.ViewModels {
OnRequestCloseDialog(false);
}
public event Shared.RequestCloseDialogEventHandler? RequestCloseDialog;
private void OnRequestCloseDialog(bool result) {
RequestCloseDialog?.Invoke(new Shared.RequestCloseDialogEventArgs { Result = result });
}
#endregion
}
}

View File

@@ -5,10 +5,12 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:BallanceTasEditor.Frontend.Views"
xmlns:vm="clr-namespace:BallanceTasEditor.Frontend.ViewModels"
xmlns:conveter="clr-namespace:BallanceTasEditor.Frontend.Converters"
xmlns:shared="clr-namespace:BallanceTasEditor.Frontend.Shared"
xmlns:widget="clr-namespace:BallanceTasEditor.Frontend.Widgets"
d:DataContext="{d:DesignInstance vm:PreferenceDialog}"
mc:Ignorable="d" WindowStartupLocation="CenterOwner" ResizeMode="NoResize" ShowInTaskbar="False"
Title="Editor Preference" Height="450" Width="400" Icon="/Frontend/Assets/Preference.ico">
Title="Editor Preference" Height="500" Width="400" Icon="/Frontend/Assets/Preference.ico">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
@@ -17,7 +19,7 @@
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Visible">
<StackPanel Orientation="Vertical">
<widget:IconGroupBox GroupBoxText="Editor Layout"
<widget:IconGroupBox GroupBoxText="Sequence Kind"
GroupBoxIcon="/Frontend/Assets/EditorLayout.ico"
Margin="10" Padding="10">
<Grid>
@@ -25,26 +27,56 @@
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<RadioButton Content="Horizontal Layout" GroupName="EditorLayout" Grid.Column="0"/>
<RadioButton Content="Vertical Layout" GroupName="EditorLayout" Grid.Column="1"/>
<RadioButton Content="Array" GroupName="SequenceKind"
ToolTip="Good for browsing. Bad for frequently inserting and removing frames."
IsChecked="{Binding SequenceKind, Mode=TwoWay, Converter={x:Static conveter:SequenceKindRadioButtonConverter.Instance}, ConverterParameter={x:Static shared:SequenceKind.Array}}"
Grid.Column="0"/>
<RadioButton Content="Double Linked List" GroupName="SequenceKind"
ToolTip="Good for frequently inserting and removing frames. Bad for browsing."
IsChecked="{Binding SequenceKind, Mode=TwoWay, Converter={x:Static conveter:SequenceKindRadioButtonConverter.Instance}, ConverterParameter={x:Static shared:SequenceKind.DoubleLinkedList}}"
Grid.Column="1"/>
</Grid>
</widget:IconGroupBox>
<widget:IconGroupBox GroupBoxText="Paste Behavior"
GroupBoxIcon="/Frontend/Assets/PasteFrame.ico"
Margin="10" Padding="10">
<widget:IconGroupBox GroupBoxText="Editor Layout"
GroupBoxIcon="/Frontend/Assets/EditorLayout.ico"
Margin="10" Padding="10"
IsEnabled="False">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<RadioButton Content="Insert Paste" GroupName="PasteBehavior" Grid.Column="0"/>
<RadioButton Content="Overwritten Paste" GroupName="PasteBehavior" Grid.Column="1"/>
<RadioButton Content="Horizontal Layout" GroupName="EditorLayout"
IsChecked="{Binding LayoutKind, Mode=TwoWay, Converter={x:Static conveter:EditorLayoutKindRadioButtonConverter.Instance}, ConverterParameter={x:Static shared:EditorLayoutKind.Horizontal}}"
Grid.Column="0"/>
<RadioButton Content="Vertical Layout" GroupName="EditorLayout"
IsChecked="{Binding LayoutKind, Mode=TwoWay, Converter={x:Static conveter:EditorLayoutKindRadioButtonConverter.Instance}, ConverterParameter={x:Static shared:EditorLayoutKind.Vertical}}"
Grid.Column="1"/>
</Grid>
</widget:IconGroupBox>
<widget:IconGroupBox GroupBoxText="Paste Behavior"
GroupBoxIcon="/Frontend/Assets/PasteFrame.ico"
Margin="10" Padding="10"
IsEnabled="False">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<RadioButton Content="Insert Paste" GroupName="PasteBehavior"
IsChecked="{Binding PasteBehavior, Mode=TwoWay, Converter={x:Static conveter:EditorPasteBehaviorRadioButtonConverter.Instance}, ConverterParameter={x:Static shared:EditorPasteBehavior.Insert}}"
Grid.Column="0"/>
<RadioButton Content="Overwritten Paste" GroupName="PasteBehavior"
IsChecked="{Binding PasteBehavior, Mode=TwoWay, Converter={x:Static conveter:EditorPasteBehaviorRadioButtonConverter.Instance}, ConverterParameter={x:Static shared:EditorPasteBehavior.Override}}"
Grid.Column="1"/>
</Grid>
</widget:IconGroupBox>
<widget:IconGroupBox GroupBoxText="Frame Count"
GroupBoxIcon="/Frontend/Assets/Count.ico"
Margin="10" Padding="10">
<TextBox Padding="3"/>
<TextBox Padding="3"
Text="{Binding FrameCount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
Style="{StaticResource TextBoxWithErrorNotifyStyle}"/>
</widget:IconGroupBox>
<widget:IconGroupBox GroupBoxText="Game Path"
GroupBoxIcon="/Frontend/Assets/SaveFileThenRunGame.ico"