1
0

6 Commits

Author SHA1 Message Date
6bcc12bf71 feat: update icon for editor 2026-04-20 22:41:31 +08:00
394768d862 feat: add pref window view model 2026-04-20 10:45:16 +08:00
6cb984fa92 feat: add converter for visibility 2026-04-19 16:01:35 +08:00
b938fe6acc feat: add CanExecute for command 2026-04-19 15:35:01 +08:00
5b230e40fc feat: add some commands in main window 2026-04-18 22:31:21 +08:00
c7578cc5a7 feat: add editor setting 2026-04-15 14:15:10 +08:00
19 changed files with 876 additions and 132 deletions

View File

@@ -3,7 +3,7 @@
viewBox="0 0 24 24"
version="1.1"
id="svg1"
sodipodi:docname="Cancel.svg"
sodipodi:docname="FieldError.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
@@ -31,11 +31,15 @@
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 12,2 C 17.5,2 22,6.5 22,12 22,17.5 17.5,22 12,22 6.5,22 2,17.5 2,12 2,6.5 6.5,2 12,2 m 0,2 C 10.1,4 8.4,4.6 7.1,5.7 L 18.3,16.9 C 19.3,15.5 20,13.8 20,12 20,7.6 16.4,4 12,4 M 16.9,18.3 5.7,7.1 C 4.6,8.4 4,10.1 4,12 c 0,4.4 3.6,8 8,8 1.9,0 3.6,-0.6 4.9,-1.7 z"
d="M 12,20 C 7.59,20 4,16.41 4,12 4,7.59 7.59,4 12,4 c 4.41,0 8,3.59 8,8 0,4.41 -3.59,8 -8,8 M 12,2 C 6.47,2 2,6.47 2,12 2,17.53 6.47,22 12,22 17.53,22 22,17.53 22,12 22,6.47 17.53,2 12,2 M 14.59,8 12,10.59 9.41,8 8,9.41 10.59,12 8,14.59 9.41,16 12,13.41 14.59,16 16,14.59 13.41,12 16,9.41 Z"
id="path1"
style="display:none" />
<path
style="fill:#f44336;fill-opacity:1;stroke-width:0.000911392"
d="M 11.529114,21.976736 C 10.22238,21.889141 9.0970988,21.622033 8.0114321,21.141743 5.0411839,19.827731 2.8549818,17.127924 2.2192797,13.988839 2.0648176,13.226108 2.0373265,12.925617 2.0373265,12 c 0,-0.925617 0.027491,-1.226108 0.1819532,-1.988839 C 2.9672398,6.3177478 5.8643146,3.2688861 9.5104706,2.3379617 10.411349,2.1079524 11.074436,2.0255288 12.01519,2.026618 c 1.996985,0.00231 3.807406,0.5499071 5.467923,1.6538737 2.455605,1.6325675 4.010624,4.1204917 4.426934,7.0827903 0.07357,0.523513 0.07357,1.949923 0,2.473436 -0.326722,2.324832 -1.359557,4.372036 -2.997659,5.94173 -1.596575,1.5299 -3.611736,2.485613 -5.773541,2.738166 -0.389906,0.04555 -1.312862,0.08002 -1.609733,0.06012 z m 1.563357,-2.032544 c 0.925767,-0.122293 1.765668,-0.379382 2.5417,-0.778003 0.341195,-0.175259 0.942023,-0.568381 1.150639,-0.752863 l 0.136709,-0.120893 -5.605032,-5.606976 C 8.2337189,9.6016202 5.704605,7.0785217 5.6962335,7.0785715 5.6878621,7.0786214 5.5669726,7.2324188 5.4275903,7.4203437 4.7111807,8.3862562 4.2640759,9.494702 4.0680851,10.790778 c -0.065181,0.431036 -0.090079,1.53472 -0.044835,1.987446 0.2406062,2.407596 1.546148,4.562332 3.5868762,5.919974 0.4303689,0.286313 1.2614491,0.698786 1.7409864,0.864068 0.6315864,0.217689 1.3326383,0.369549 1.9653433,0.425729 0.425722,0.0378 1.326427,0.01559 1.776015,-0.0438 z m 5.537897,-3.505461 c 0.686116,-1.101759 1.081879,-2.107709 1.286142,-3.269111 0.0819,-0.465656 0.111841,-1.431584 0.06035,-1.946773 C 19.735881,8.811741 18.433266,6.6619006 16.388891,5.3012482 15.724197,4.8588544 14.767203,4.4347242 13.990518,4.2383123 12.090514,3.75783 10.037364,3.9752015 8.3658289,4.8338107 8.0246342,5.0090703 7.4238064,5.4021922 7.2151899,5.5866738 L 7.078481,5.707567 l 5.605063,5.608114 5.605064,5.608114 0.07595,-0.09009 c 0.04177,-0.04955 0.161387,-0.227288 0.265811,-0.394971 z"
style="fill:#f44336;stroke-width:0.000911392;fill-opacity:1"
d="M 11.498734,21.97371 C 10.538494,21.927417 9.4258037,21.695256 8.4759494,21.34301 8.0845522,21.197863 7.2207324,20.778082 6.8506329,20.553172 6.0186267,20.047561 5.2728592,19.437005 4.6411761,18.744304 2.5685315,16.471448 1.6637184,13.447187 2.1463612,10.405597 2.5810326,7.6663212 4.1740918,5.2042016 6.5063119,3.6671662 7.8220107,2.8000629 9.2792591,2.2701711 10.875949,2.0782573 c 0.574874,-0.069097 1.673228,-0.069097 2.248102,0 3.266567,0.3926243 6.042908,2.2588082 7.635954,5.13269 C 21.54949,8.63519 21.979539,10.323974 21.979539,12 c 0,3.054298 -1.366007,5.885584 -3.773974,7.822213 -0.621166,0.499579 -1.583784,1.066107 -2.374062,1.397204 -1.3282,0.556467 -2.869699,0.824827 -4.332769,0.754293 z m 1.112044,-1.98475 c 3.561075,-0.284458 6.493751,-2.850098 7.226887,-6.322415 C 20.26315,11.651338 19.880556,9.4662 18.803847,7.7620253 17.492724,5.6868267 15.384677,4.3473261 12.941772,4.0371303 12.549756,3.9873527 11.449156,3.9874453 11.058228,4.0372887 9.198254,4.2744355 7.5140166,5.1092443 6.254174,6.4184649 4.9923609,7.7297335 4.2458673,9.3098233 4.0364135,11.112759 c -0.059996,0.516438 -0.042475,1.588281 0.033872,2.072051 0.2243066,1.421314 0.78997,2.713286 1.6759437,3.827848 0.2493782,0.31372 0.8842563,0.953423 1.195543,1.204629 1.6091067,1.298537 3.6413238,1.933644 5.6690058,1.771673 z"
id="path2" />
<path
style="fill:#f44336;fill-opacity:1;stroke-width:0.000911392"
d="M 8.7189889,15.281014 8.0206349,14.582165 9.3114492,13.291082 10.602264,12 9.3114492,10.708918 8.0206349,9.4178352 8.7189889,8.7189858 9.4173428,8.0201364 10.708804,9.3113356 12.000265,10.602535 13.291164,9.3113678 14.582063,8.0202008 15.280797,8.718935 15.979531,9.4176691 14.688634,10.708835 13.397736,12 l 1.290898,1.291165 1.290897,1.291166 -0.698734,0.698734 -0.698734,0.698734 -1.290899,-1.291167 -1.290899,-1.291167 -1.291461,1.291199 -1.2914612,1.291199 z"
id="path3" />
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 24 24"
version="1.1"
id="svg1"
sodipodi:docname="SequenceLayout.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="32.916667"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="m 15,7 v 9 H 10 V 7 h 5 m 6,-2 h -3 v 13 h 3 V 5 M 17,5 H 8 v 13 h 9 V 5 M 7,5 H 4 v 13 h 3 z"
id="path1"
style="display:none" />
<path
style="fill:#607d8b;fill-opacity:1;stroke-width:0.000911392"
d="M 4.0253165,11.498734 V 5.0126582 H 5.4987342 6.9721519 V 11.498734 17.98481 H 5.4987342 4.0253165 Z"
id="path2" />
<path
style="fill:#607d8b;fill-opacity:1;stroke-width:0.000911392"
d="M 8.0050633,11.498734 V 5.0126582 h 4.4962027 4.496202 V 11.498734 17.98481 H 12.501266 8.0050633 Z m 7.0177217,0 V 6.9873418 H 12.501266 9.9797468 v 4.5113922 4.511393 h 2.5215192 2.521519 z"
id="path3" />
<path
style="fill:#607d8b;fill-opacity:1;stroke-width:0.000911392"
d="M 18,11.498734 V 5.0126582 h 1.488608 1.488607 V 11.498734 17.98481 H 19.488608 18 Z"
id="path4" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -16,8 +16,8 @@
<ItemGroup>
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="OneOf" Version="3.0.271" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.5" />
</ItemGroup>
</Project>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

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

@@ -0,0 +1,29 @@
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 {
[ValueConversion(typeof(bool), typeof(Visibility))]
public class IsVisibleToVisibilityConverter : IValueConverter {
public static readonly IsVisibleToVisibilityConverter Instance = new IsVisibleToVisibilityConverter();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var susBool = value as bool?;
if (susBool is null) {
return Binding.DoNothing;
} else {
return susBool.Value ? Visibility.Visible : Visibility.Collapsed;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return Binding.DoNothing;
}
}
}

View File

@@ -0,0 +1,67 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BallanceTasEditor.Frontend.Models {
public partial class EditorSetting : ObservableObject {
public EditorSetting() {
// YYC MARK:
// Due to the shitty conflict between CommunityToolkit.Mvvm and Nullable aware,
// I was forcely set this field to clear all warning.
GamePath = "";
// Initialize from singleton.
FromSingleton();
}
[ObservableProperty]
private Shared.SequenceKind sequenceKind;
[ObservableProperty]
private Shared.EditorLayoutKind editorLayout;
[ObservableProperty]
private Shared.EditorPasteBehavior pasteBehavior;
[ObservableProperty]
private int frameCount;
[ObservableProperty]
private string gamePath;
[MemberNotNull(nameof(SequenceKind))]
[MemberNotNull(nameof(EditorLayout))]
[MemberNotNull(nameof(PasteBehavior))]
[MemberNotNull(nameof(FrameCount))]
[MemberNotNull(nameof(GamePath))]
private void FromSingleton() {
var singleton = Shared.EditorConfiguration.Instance;
SequenceKind = singleton.SequenceKind;
EditorLayout = singleton.EditorLayout;
PasteBehavior = singleton.PasteBehavior;
FrameCount = singleton.FrameCount;
GamePath = singleton.GamePath;
}
private void ToSingleton() {
var singleton = Shared.EditorConfiguration.Instance;
singleton.SequenceKind = SequenceKind;
singleton.EditorLayout = EditorLayout;
singleton.PasteBehavior = PasteBehavior;
singleton.FrameCount = FrameCount;
singleton.GamePath = GamePath;
}
public void Save() {
ToSingleton();
Shared.EditorConfiguration.Instance.Save();
}
}
}

View File

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

View File

@@ -1,25 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace BallanceTasEditor.Frontend.Models {
public enum TasSequenceKind {
Array,
DoubleLinkedList
}
public static class TasSequenceKindHelper {
public static Backend.ITasSequence CreateSequenceByKind(TasSequenceKind kind) {
return kind switch {
TasSequenceKind.Array => new Backend.ListTasSequence(),
TasSequenceKind.DoubleLinkedList => new Backend.LegacyTasSequence(),
_ => throw new UnreachableException(),
};
}
}
}

View File

@@ -17,7 +17,9 @@ namespace BallanceTasEditor.Frontend.Shared {
bool ShowConfirmExitWhenOpeningFileDialog();
bool ShowFileChangedDialog();
GotoDialogResult? ShowGotoDialog();
bool ShowConfirmClearKeysDialog();
EditFpsDialogResult? ShowEditFpsDialog();
bool ShowConfirmUniformFpsDialog();
AddFrameDialogResult? ShowAddFrameDialog();
PreferenceDialogResult? ShowPreferenceDialog();
void ShowManuallyReportBugDialog();
@@ -133,6 +135,14 @@ namespace BallanceTasEditor.Frontend.Shared {
}
}
public bool ShowConfirmClearKeysDialog() {
var rv = MessageBox.Show(
"Do you really want to clear keys for all frames?\nThis operation can not be revoked.",
"Clear Keys",
MessageBoxButton.YesNo, MessageBoxImage.Question);
return rv == MessageBoxResult.Yes;
}
public EditFpsDialogResult? ShowEditFpsDialog() {
var dialog = new Views.EditFpsDialog();
dialog.Owner = m_Parent;
@@ -144,6 +154,14 @@ namespace BallanceTasEditor.Frontend.Shared {
}
}
public bool ShowConfirmUniformFpsDialog() {
var rv = MessageBox.Show(
"Do you really want to set an uniform FPS value for all frames?\nThis operation can not be revoked.",
"Uniform FPS",
MessageBoxButton.YesNo, MessageBoxImage.Question);
return rv == MessageBoxResult.Yes;
}
public AddFrameDialogResult? ShowAddFrameDialog() {
var dialog = new Views.AddFrameDialog();
dialog.Owner = m_Parent;

View File

@@ -0,0 +1,185 @@
using BallanceTasEditor.Frontend.Models;
using CommunityToolkit.Mvvm.ComponentModel;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BallanceTasEditor.Frontend.Shared {
public class EditorConfiguration {
public static readonly EditorConfiguration Instance = new EditorConfiguration();
private EditorConfiguration() {
Init();
}
public SequenceKind SequenceKind { get; set; }
public EditorLayoutKind EditorLayout { 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(PasteBehavior))]
[MemberNotNull(nameof(FrameCount))]
[MemberNotNull(nameof(GamePath))]
private void FromRaw(RawEditorConfiguration raw) {
SequenceKind = raw.SequenceKind;
EditorLayout = raw.EditorLayout;
PasteBehavior = raw.PasteBehavior;
FrameCount = raw.FrameCount;
GamePath = raw.GamePath;
}
private RawEditorConfiguration ToRaw() {
return new RawEditorConfiguration {
SequenceKind = SequenceKind,
EditorLayout = EditorLayout,
PasteBehavior = PasteBehavior,
FrameCount = FrameCount,
GamePath = GamePath
};
}
private string GetSettingFilePath() {
string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
string selfAppDataPath = Path.Combine(appDataPath, "BallanceTasEditor");
return Path.Combine(selfAppDataPath, "BallanceTasEditor.cfg");
}
private bool Load() {
try {
using (var fs = new StreamReader(GetSettingFilePath(), Encoding.UTF8)) {
// Read semester object.
var raw = JsonConvert.DeserializeObject<RawEditorConfiguration>(fs.ReadToEnd()).Unwrap();
// Apply to self.
FromRaw(raw);
}
return true;
} catch (Exception) {
return false;
}
}
[MemberNotNull(nameof(SequenceKind))]
[MemberNotNull(nameof(EditorLayout))]
[MemberNotNull(nameof(PasteBehavior))]
[MemberNotNull(nameof(FrameCount))]
[MemberNotNull(nameof(GamePath))]
private void Init() {
var defaultRaw = new RawEditorConfiguration();
FromRaw(defaultRaw);
}
public void LoadOrInit() {
if (!Load()) {
Init();
}
}
public void Save() {
try {
// Check file path directory.
var settingFilePath = GetSettingFilePath();
var settingFileDirectory = Path.GetDirectoryName(settingFilePath).Unwrap();
if (!Directory.Exists(settingFileDirectory)) {
Directory.CreateDirectory(settingFileDirectory);
}
// Write it.
using (var fs = new StreamWriter(GetSettingFilePath(), false, Encoding.UTF8)) {
fs.Write(JsonConvert.SerializeObject(ToRaw()));
}
} catch (Exception) {
// Do nothing.
}
}
private class RawEditorConfiguration {
[JsonProperty("sequence_kind", Required = Required.Always)]
[JsonConverter(typeof(TasSequenceKindConverter))]
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_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;
[JsonProperty("game_path", Required = Required.Always)]
public string GamePath { get; set; } = "";
}
#region Custom JSON Converter
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 (SequenceKindHelper.TryParse(value, out var kind)) {
return kind;
} else {
throw new JsonSerializationException($"given string can not be parsed as TasSequenceKind: {value}");
}
} else {
throw new JsonSerializationException($"expect a integer but got {reader.TokenType}");
}
}
public override void WriteJson(JsonWriter writer, SequenceKind value, JsonSerializer serializer) {
writer.WriteValue(SequenceKindHelper.ToString(value));
}
}
private class EditorLayoutKindConverter : JsonConverter<EditorLayoutKind> {
public override EditorLayoutKind ReadJson(JsonReader reader, Type objectType, EditorLayoutKind existingValue, bool hasExistingValue, JsonSerializer serializer) {
if (reader.TokenType == JsonToken.String) {
var value = (reader.Value as string).Unwrap();
if (EditorLayoutKindHelper.TryParse(value, out var kind)) {
return kind;
} else {
throw new JsonSerializationException($"given string can not be parsed as EditorLayoutKind: {value}");
}
} else {
throw new JsonSerializationException($"expect a integer but got {reader.TokenType}");
}
}
public override void WriteJson(JsonWriter writer, EditorLayoutKind value, JsonSerializer serializer) {
writer.WriteValue(EditorLayoutKindHelper.ToString(value));
}
}
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 (EditorPasteBehaviorHelper.TryParse(value, out var kind)) {
return kind;
} else {
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, EditorPasteBehavior value, JsonSerializer serializer) {
writer.WriteValue(EditorPasteBehaviorHelper.ToString(value));
}
}
#endregion
}
}

View File

@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BallanceTasEditor.Frontend.Shared {
public enum SequenceKind {
Array,
DoubleLinkedList
}
public static class SequenceKindHelper {
public static Backend.ITasSequence CreateSequenceByKind(SequenceKind kind) {
return kind switch {
SequenceKind.Array => new Backend.ListTasSequence(),
SequenceKind.DoubleLinkedList => new Backend.LegacyTasSequence(),
_ => throw new UnreachableException(),
};
}
public static bool TryParse(string s, out SequenceKind value) {
switch (s) {
case "array":
value = SequenceKind.Array;
return true;
case "double_linked_list":
value = SequenceKind.DoubleLinkedList;
return true;
default:
value = default;
return false;
}
}
public static string ToString(SequenceKind kind) {
return kind switch {
SequenceKind.Array => "array",
SequenceKind.DoubleLinkedList => "double_linked_list",
_ => throw new UnreachableException(),
};
}
}
public enum EditorLayoutKind {
Vertical,
Horizontal
}
public static class EditorLayoutKindHelper {
public static bool TryParse(string s, out EditorLayoutKind value) {
switch (s) {
case "vertical":
value = EditorLayoutKind.Vertical;
return true;
case "horizontal":
value = EditorLayoutKind.Horizontal;
return true;
default:
value = default;
return false;
}
}
public static string ToString(EditorLayoutKind kind) {
return kind switch {
EditorLayoutKind.Vertical => "vertical",
EditorLayoutKind.Horizontal => "horizontal",
_ => throw new UnreachableException(),
};
}
}
public enum EditorPasteBehavior {
Insert,
Override
}
public static class EditorPasteBehaviorHelper {
public static bool TryParse(string s, out EditorPasteBehavior value) {
switch (s) {
case "insert":
value = EditorPasteBehavior.Insert;
return true;
case "override":
value = EditorPasteBehavior.Override;
return true;
default:
value = default;
return false;
}
}
public static string ToString(EditorPasteBehavior mode) {
return mode switch {
EditorPasteBehavior.Insert => "insert",
EditorPasteBehavior.Override => "override",
_ => throw new UnreachableException(),
};
}
}
}

View File

@@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTemplate x:Key="TextBoxErrorTemplate">
<Grid>
<Image Source="/Frontend/Assets/Cancel.ico" RenderOptions.BitmapScalingMode="HighQuality"
<Image Source="/Frontend/Assets/FieldError.ico" RenderOptions.BitmapScalingMode="HighQuality"
Width="16" Height="16" Margin="0, 0, 4, 0" VerticalAlignment="Center" HorizontalAlignment="Right">
<Image.ToolTip>
<ToolTip>

View File

@@ -33,7 +33,6 @@ namespace BallanceTasEditor.Frontend.ViewModels {
#region File Operation
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(WindowTitle))]
private Models.TasFile tasFile;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(WindowTitle))]
@@ -42,11 +41,16 @@ namespace BallanceTasEditor.Frontend.ViewModels {
private void TasFile_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) {
// YYC MARK:
// Due to the shitty limit of MVVM Toolkit,
// I was forced trigger these command manually.
// I was forced trigger these manually.
OnPropertyChanged(nameof(IsVisibleForLoadedFile));
OnPropertyChanged(nameof(WindowTitle));
OnPropertyChanged(nameof(IsVisibleForNotLoadedFile));
NewFileCommand.NotifyCanExecuteChanged();
OpenFileCommand.NotifyCanExecuteChanged();
SaveFileCommand.NotifyCanExecuteChanged();
SaveFileAsCommand.NotifyCanExecuteChanged();
SaveFileThenRunGameCommand.NotifyCanExecuteChanged();
CloseFileCommand.NotifyCanExecuteChanged();
}
@@ -57,7 +61,7 @@ namespace BallanceTasEditor.Frontend.ViewModels {
if (dialog is null) return;
// Create new file
TasFile.NewFile(Models.TasSequenceKind.Array, dialog.Count, dialog.Fps);
TasFile.NewFile(Shared.SequenceKind.Array, dialog.Count, dialog.Fps);
// Set members
TasFilePath = null;
// Send notification
@@ -76,7 +80,7 @@ namespace BallanceTasEditor.Frontend.ViewModels {
// Load file
try {
TasFile.LoadFile(Models.TasSequenceKind.Array, dialog.Path);
TasFile.LoadFile(Shared.SequenceKind.Array, dialog.Path);
} catch (Exception e) {
m_DialogService.ShowOpenFileFailedDialog(e);
return;
@@ -159,6 +163,19 @@ namespace BallanceTasEditor.Frontend.ViewModels {
#endregion
#region Special Save with Running Game
[RelayCommand(CanExecute = nameof(CanSaveFileThenRunGame))]
private void SaveFileThenRunGame() {
SaveFile();
}
private bool CanSaveFileThenRunGame() {
return CanSaveFile() && true;
}
#endregion
#region Exit Stuff
[RelayCommand]
@@ -179,6 +196,133 @@ namespace BallanceTasEditor.Frontend.ViewModels {
#region Edit Menu
#region Undo and Redo
[RelayCommand(CanExecute = nameof(CanUndo))]
private void Undo() {
}
private bool CanUndo() {
return true;
}
[RelayCommand(CanExecute = nameof(CanRedo))]
private void Redo() {
}
private bool CanRedo() {
return true;
}
#endregion
#region Viewer Operation
[RelayCommand(CanExecute = nameof(CanPreviousPage))]
private void PreviousPage() {
}
private bool CanPreviousPage() {
return true;
}
[RelayCommand(CanExecute = nameof(CanPreviousItem))]
private void PreviousItem() {
}
private bool CanPreviousItem() {
return true;
}
[RelayCommand(CanExecute = nameof(CanNextPage))]
private void NextPage() {
}
private bool CanNextPage() {
return true;
}
[RelayCommand(CanExecute = nameof(CanNextItem))]
private void NextItem() {
}
private bool CanNextItem() {
return true;
}
[RelayCommand(CanExecute = nameof(CanGoto))]
private void Goto() {
}
private bool CanGoto() {
return true;
}
#endregion
#region Tool Mode
[RelayCommand(CanExecute = nameof(CanSelectMode))]
private void SelectMode() {
}
private bool CanSelectMode() {
return true;
}
[RelayCommand(CanExecute = nameof(CanFillMode))]
private void FillMode() {
}
private bool CanFillMode() {
return true;
}
[RelayCommand(CanExecute = nameof(CanDrawMode))]
private void DrawMode() {
}
private bool CanDrawMode() {
return true;
}
#endregion
#region Misc Edit Operations
[RelayCommand(CanExecute = nameof(CanUniformFps))]
private void ClearKeys() {
m_DialogService.ShowConfirmClearKeysDialog();
}
private bool CanClearKeys() {
return true;
}
[RelayCommand(CanExecute = nameof(CanUniformFps))]
private void UniformFps() {
var rv = m_DialogService.ShowEditFpsDialog();
if (rv is not null) {
m_DialogService.ShowConfirmUniformFpsDialog();
}
}
private bool CanUniformFps() {
return true;
}
#endregion
#region Preference
@@ -216,6 +360,9 @@ namespace BallanceTasEditor.Frontend.ViewModels {
[ObservableProperty]
private string statusMessage;
/// <summary>
/// The UI thread timer for dimming status message after specific duration.
/// </summary>
private DispatcherTimer m_StatusMessageDimmer;
private void UpdateStatusMessage(string msg) {
@@ -248,6 +395,14 @@ namespace BallanceTasEditor.Frontend.ViewModels {
}
}
public bool IsVisibleForLoadedFile {
get => TasFile.IsFileLoaded;
}
public bool IsVisibleForNotLoadedFile {
get => TasFile.IsFileNotLoaded;
}
#endregion
}

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,15 +5,15 @@
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:widget="clr-namespace:BallanceTasEditor.Frontend.Widgets"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance vm:MainWindow}"
WindowStartupLocation="CenterScreen"
Title="{Binding WindowTitle, Mode=OneWay}" Height="600" Width="800" Icon="/Frontend/Assets/App.ico">
Title="{Binding WindowTitle, Mode=OneWay}" Height="800" Width="600" Icon="/Frontend/Assets/App.ico">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
@@ -52,27 +52,27 @@
<Separator/>
<MenuItem Header="_Save File" Icon="{StaticResource IconSaveFile}" InputGestureText="Ctrl+S" Command="{Binding SaveFileCommand}"/>
<MenuItem Header="Save File as ..." Icon="{StaticResource IconSaveFileAs}" Command="{Binding SaveFileAsCommand}"/>
<MenuItem Header="Save File then _Run Game" Icon="{StaticResource IconSaveFileThenRunGame}" InputGestureText="B"/>
<MenuItem Header="Save File then _Run Game" Icon="{StaticResource IconSaveFileThenRunGame}" InputGestureText="B" Command="{Binding SaveFileThenRunGameCommand}"/>
<Separator/>
<MenuItem Header="Close File" Icon="{StaticResource IconCloseFile}" Command="{Binding CloseFileCommand}"/>
<MenuItem Header="Exit" Icon="{StaticResource IconExit}" Command="{Binding ExitCommand}"/>
</MenuItem>
<MenuItem Header="_Edit" Padding="5">
<MenuItem Header="_Undo" Icon="{StaticResource IconUndo}" InputGestureText="Ctrl+Z"/>
<MenuItem Header="_Redo" Icon="{StaticResource IconRedo}" InputGestureText="Ctrl+Y"/>
<MenuItem Header="_Undo" Icon="{StaticResource IconUndo}" InputGestureText="Ctrl+Z" Command="{Binding UndoCommand}"/>
<MenuItem Header="_Redo" Icon="{StaticResource IconRedo}" InputGestureText="Ctrl+Y" Command="{Binding RedoCommand}"/>
<Separator/>
<MenuItem Header="Previous Page" Icon="{StaticResource IconPreviousPage}" InputGestureText="A"/>
<MenuItem Header="Previous Item" Icon="{StaticResource IconPreviousItem}" InputGestureText="S"/>
<MenuItem Header="Next Page" Icon="{StaticResource IconNextPage}" InputGestureText="D"/>
<MenuItem Header="Next Item" Icon="{StaticResource IconNextItem}" InputGestureText="F"/>
<MenuItem Header="Goto Item" Icon="{StaticResource IconGoto}" InputGestureText="G"/>
<MenuItem Header="Previous Page" Icon="{StaticResource IconPreviousPage}" InputGestureText="A" Command="{Binding PreviousPageCommand}"/>
<MenuItem Header="Previous Item" Icon="{StaticResource IconPreviousItem}" InputGestureText="S" Command="{Binding PreviousItemCommand}"/>
<MenuItem Header="Next Page" Icon="{StaticResource IconNextPage}" InputGestureText="D" Command="{Binding NextPageCommand}"/>
<MenuItem Header="Next Item" Icon="{StaticResource IconNextItem}" InputGestureText="F" Command="{Binding NextItemCommand}"/>
<MenuItem Header="Goto Item" Icon="{StaticResource IconGoto}" InputGestureText="G" Command="{Binding GotoCommand}"/>
<Separator/>
<MenuItem Header="Select Mode" Icon="{StaticResource IconSelectMode}" InputGestureText="Q"/>
<MenuItem Header="Fill Mode" Icon="{StaticResource IconFillMode}" InputGestureText="W"/>
<MenuItem Header="Draw Mode" Icon="{StaticResource IconDrawMode}" InputGestureText="E"/>
<MenuItem Header="Select Mode" Icon="{StaticResource IconSelectMode}" InputGestureText="Q" Command="{Binding SelectModeCommand}"/>
<MenuItem Header="Fill Mode" Icon="{StaticResource IconFillMode}" InputGestureText="W" Command="{Binding FillModeCommand}"/>
<MenuItem Header="Draw Mode" Icon="{StaticResource IconDrawMode}" InputGestureText="E" Command="{Binding DrawModeCommand}"/>
<Separator/>
<MenuItem Header="Clear Keys" Icon="{StaticResource IconClearKeys}"/>
<MenuItem Header="Uniform FPS" Icon="{StaticResource IconUniformFps}"/>
<MenuItem Header="Clear Keys" Icon="{StaticResource IconClearKeys}" Command="{Binding ClearKeysCommand}"/>
<MenuItem Header="Uniform FPS" Icon="{StaticResource IconUniformFps}" Command="{Binding UniformFpsCommand}"/>
<Separator/>
<MenuItem Header="Preference" Icon="{StaticResource IconPreference}" InputGestureText="Ctrl+P" Command="{Binding PreferenceCommand}"/>
</MenuItem>
@@ -82,7 +82,23 @@
</MenuItem>
</Menu>
<Grid Grid.Row="1">
<Grid Grid.Row="1" Visibility="{Binding IsVisibleForNotLoadedFile, Mode=OneWay, Converter={x:Static conveter:IsVisibleToVisibilityConverter.Instance}}">
<Grid VerticalAlignment="Center" HorizontalAlignment="Center" AllowDrop="True">
<Rectangle StrokeThickness="4" Stroke="Gray" StrokeDashArray="4 4" Fill="Transparent"/>
<StackPanel Orientation="Horizontal" Margin="20">
<Image Source="/Frontend/Assets/OpenFile.ico" Width="24" Height="24" Margin="5" VerticalAlignment="Center"/>
<TextBlock Margin="5" Text="Create, Open or Drop a TAS File in There for Editing" Foreground="Gray" FontSize="16" VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</Grid>
<Grid Grid.Row="1" Visibility="{Binding IsVisibleForLoadedFile, Mode=OneWay, Converter={x:Static conveter:IsVisibleToVisibilityConverter.Instance}}">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="auto"/>
@@ -91,30 +107,26 @@
<StackPanel Orientation="Horizontal" Grid.Column="0">
<widget:IconButton ButtonText="Select Mode"
ButtonIcon="/Frontend/Assets/SelectMode.ico"
Margin="5" Padding="5"/>
Margin="5" Padding="5"
Command="{Binding SelectModeCommand}"/>
<widget:IconButton ButtonText="Fill Mode"
ButtonIcon="/Frontend/Assets/FillMode.ico"
Margin="5" Padding="5"/>
Margin="5" Padding="5"
Command="{Binding FillModeCommand}"/>
<widget:IconButton ButtonText="Draw Mode"
ButtonIcon="/Frontend/Assets/DrawMode.ico"
Margin="5" Padding="5"/>
Margin="5" Padding="5"
Command="{Binding DrawModeCommand}"/>
</StackPanel>
<widget:IconButton ButtonText="Save File then Run Game"
ButtonIcon="/Frontend/Assets/SaveFileThenRunGame.ico"
Grid.Column="1" Margin="5" Padding="5"/>
Grid.Column="1" Margin="5" Padding="5"
Command="{Binding SaveFileThenRunGameCommand}"/>
</Grid>
<Grid Grid.Row="2">
<Grid VerticalAlignment="Center" HorizontalAlignment="Center" AllowDrop="True">
<Rectangle StrokeThickness="4" Stroke="Gray" StrokeDashArray="4 4" Fill="Transparent"/>
<StackPanel Orientation="Horizontal" Margin="20">
<Image Source="/Frontend/Assets/OpenFile.ico" Width="24" Height="24" Margin="5" VerticalAlignment="Center"/>
<TextBlock Margin="5" Text="Create, Open or Drop a TAS File in There for Editing" Foreground="Gray" FontSize="16" VerticalAlignment="Center"/>
</StackPanel>
</Grid>
<Grid Grid.Row="1">
<local:TasViewer>
<local:TasViewer.ContextMenu>
<ContextMenu>
@@ -152,8 +164,9 @@
</local:TasViewer.ContextMenu>
</local:TasViewer>
</Grid>
</Grid>
<StatusBar Grid.Row="3">
<StatusBar Grid.Row="2">
<!-- 这玩意要逆序排列才能达到我想要的需求也是奇葩 -->
<StatusBarItem Content="v2.0 stable" DockPanel.Dock="Right" Foreground="Gray" FontStyle="Italic"/>
<Separator DockPanel.Dock="Right"/>

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,34 +19,64 @@
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Visible">
<StackPanel Orientation="Vertical">
<widget:IconGroupBox GroupBoxText="Editor Layout"
GroupBoxIcon="/Frontend/Assets/EditorLayout.ico"
<widget:IconGroupBox GroupBoxText="Sequence Kind"
GroupBoxIcon="/Frontend/Assets/SequenceKind.ico"
Margin="10" Padding="10">
<Grid>
<Grid.ColumnDefinitions>
<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="Editor Layout"
GroupBoxIcon="/Frontend/Assets/EditorLayout.ico"
Margin="10" Padding="10"
IsEnabled="False">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<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">
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="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"