diff --git a/BallanceTasEditor/BallanceTasEditor/Backend/AppSettings.cs b/BallanceTasEditor/BallanceTasEditor/Backend/AppSettings.cs new file mode 100644 index 0000000..0e2a4d2 --- /dev/null +++ b/BallanceTasEditor/BallanceTasEditor/Backend/AppSettings.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BallanceTasEditor.Backend { + + public enum EditorLayoutKind { + Horizontal, Vertical + } + + public class AppSettings { + + + + public EditorLayoutKind EditorLayout { get; set; } + + } +} diff --git a/BallanceTasEditor/BallanceTasEditor/Backend/TasFrame.cs b/BallanceTasEditor/BallanceTasEditor/Backend/TasFrame.cs index 3a907b0..d3c38c1 100644 --- a/BallanceTasEditor/BallanceTasEditor/Backend/TasFrame.cs +++ b/BallanceTasEditor/BallanceTasEditor/Backend/TasFrame.cs @@ -156,6 +156,14 @@ namespace BallanceTasEditor.Backend { raw.KeyFlags = m_KeyFlags; } + /// + /// 返回自身的克隆(深拷贝)。 + /// + /// 自身的克隆。 + public TasFrame Clone() { + return new TasFrame(m_TimeDelta, m_KeyFlags); + } + /// /// 该帧的持续时间(以秒为单位)。 /// diff --git a/BallanceTasEditor/BallanceTasEditor/Backend/TasStorage.cs b/BallanceTasEditor/BallanceTasEditor/Backend/TasStorage.cs index 7cc1d3c..675c34c 100644 --- a/BallanceTasEditor/BallanceTasEditor/Backend/TasStorage.cs +++ b/BallanceTasEditor/BallanceTasEditor/Backend/TasStorage.cs @@ -10,11 +10,31 @@ using System.Threading.Tasks; namespace BallanceTasEditor.Backend { public static class TasStorage { + + /// + /// Initialize given TAS sequence with given count frame which has given FPS. + /// + /// The TAS sequence to initialize. + /// The count of frame. + /// The FPS of frame. + public static void Init(ITasSequence seq, int count, uint fps) { + var frame = TasFrame.FromFps(fps); + var iter = Enumerable.Range(0, count).Select((_) => frame.Clone()); + var exactSizeIter = new ExactSizeEnumerableAdapter(iter, count); + seq.Insert(seq.GetCount(), exactSizeIter); + } + internal const int SIZEOF_I32 = sizeof(int); internal const int SIZEOF_F32 = sizeof(float); internal const int SIZEOF_U32 = sizeof(uint); internal const int SIZEOF_RAW_TAS_FRAME = SIZEOF_F32 + SIZEOF_U32; + /// + /// Save given TAS sequence into given file path. + /// + /// The path to file for saving. + /// The TAS sequence to save. + /// Any exception occurs when saving. public static void Save(string filepath, ITasSequence seq) { using (var fs = new FileStream(filepath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None)) { Save(fs, seq); @@ -22,6 +42,12 @@ namespace BallanceTasEditor.Backend { } } + /// + /// Save given TAS sequence into given file stream. + /// + /// The file stream for saving. + /// The TAS sequence to save. + /// Any exception occurs when saving. public static void Save(Stream fs, ITasSequence seq) { var totalByte = seq.GetCount() * SIZEOF_RAW_TAS_FRAME; fs.Write(BitConverter.GetBytes(totalByte), 0, SIZEOF_I32); @@ -46,6 +72,12 @@ namespace BallanceTasEditor.Backend { //zo.Close(); } + /// + /// Load TAS sequence from given file path into given sequence. + /// + /// The path to file for loading. + /// The TAS sequence to load. + /// Any exception occurs when loading. public static void Load(string filepath, ITasSequence seq) { using (var fs = new FileStream(filepath, FileMode.Open, FileAccess.Read, FileShare.Read)) { Load(fs, seq); @@ -53,6 +85,12 @@ namespace BallanceTasEditor.Backend { } } + /// + /// Load TAS sequence from given file stream into given sequence. + /// + /// The file stream for loading. + /// The TAS sequence to load. + /// Any exception occurs when loading. public static void Load(Stream fs, ITasSequence seq) { // Read total bytes var lenCache = new byte[SIZEOF_I32]; diff --git a/BallanceTasEditor/BallanceTasEditor/BallanceTasEditor.csproj b/BallanceTasEditor/BallanceTasEditor/BallanceTasEditor.csproj index 54e2f8f..f14dc7d 100644 --- a/BallanceTasEditor/BallanceTasEditor/BallanceTasEditor.csproj +++ b/BallanceTasEditor/BallanceTasEditor/BallanceTasEditor.csproj @@ -17,9 +17,12 @@ + + + diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/Behaviors/ConfirmCloseBehavior.cs b/BallanceTasEditor/BallanceTasEditor/Frontend/Behaviors/ConfirmCloseBehavior.cs new file mode 100644 index 0000000..674b2c5 --- /dev/null +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/Behaviors/ConfirmCloseBehavior.cs @@ -0,0 +1,49 @@ +using Microsoft.Xaml.Behaviors; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace BallanceTasEditor.Frontend.Behaviors { + public class ConfirmCloseBehavior : Behavior { + + public ICommand ConfirmCommand { + get { return (ICommand)GetValue(ConfirmCommandProperty); } + set { SetValue(ConfirmCommandProperty, value); } + } + + // Using a DependencyProperty as the backing store for ConfirmCommand. This enables animation, styling, binding, etc... + public static readonly DependencyProperty ConfirmCommandProperty = + DependencyProperty.Register("ConfirmCommand", typeof(ICommand), typeof(ConfirmCloseBehavior)); + + + protected override void OnAttached() { + base.OnAttached(); + AssociatedObject.Closing += OnClosing; + } + + protected override void OnDetaching() { + AssociatedObject.Closing -= OnClosing; + base.OnDetaching(); + } + + private void OnClosing(object? sender, CancelEventArgs e) { + if (ConfirmCommand?.CanExecute(null) == true) { + // 假设Command返回 bool 或通过回调/事件通知结果 + bool allowClose = (Func)ConfirmCommand.Execute(null); + e.Cancel = !allowClose; + } + } + } +} diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/IDialogService.cs b/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/IDialogService.cs new file mode 100644 index 0000000..5fd4b7a --- /dev/null +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/IDialogService.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BallanceTasEditor.Frontend.ViewModels { + + public interface IDialogService { + NewFileDialogResult? ShowNewFileDialog(); + OpenFileDialogResult? ShowOpenFileDialog(); + void ShowOpenFileFailedDialog(Exception e); + SaveFileDialogResult? ShowSaveFileDialog(); + void ShowSaveFileFailedDialog(Exception e); + bool ShowConfirmCloseFileDialog(string message); + bool ShowConfirmExitWhenOpeningFileDialog(); + bool ShowFileChangedDialog(); + GotoDialogResult? ShowGotoDialog(); + EditFpsDialogResult? ShowEditFpsDialog(); + AddFrameDialogResult? ShowAddFrameDialog(); + PreferenceDialogResult? ShowPreferenceDialog(); + void ShowAboutDialog(); + } + + public record NewFileDialogResult { + public required uint Fps { get; init; } + public required int Count { get; init; } + } + + public record OpenFileDialogResult { + public required string Path { get; init; } + } + + public record SaveFileDialogResult { + public required string Path { get; init; } + } + + public record GotoDialogResult { } + + public record EditFpsDialogResult { } + + public record AddFrameDialogResult { } + + public record PreferenceDialogResult { } + + +} diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/MainWindow.cs b/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/MainWindow.cs new file mode 100644 index 0000000..2053f7d --- /dev/null +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/MainWindow.cs @@ -0,0 +1,156 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BallanceTasEditor.Frontend.ViewModels { + public partial class MainWindow : ObservableObject { + public MainWindow(IDialogService dialogService) { + m_DialogService = dialogService; + + this.TasFile = null; + this.TasFilePath = null; + } + + private IDialogService m_DialogService; + + #region File Operation + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(WindowTitle))] + [NotifyCanExecuteChangedFor(nameof(NewFileCommand))] + [NotifyCanExecuteChangedFor(nameof(OpenFileCommand))] + [NotifyCanExecuteChangedFor(nameof(SaveFileCommand))] + [NotifyCanExecuteChangedFor(nameof(SaveFileAsCommand))] + [NotifyCanExecuteChangedFor(nameof(CloseFileCommand))] + private Backend.ITasSequence? tasFile; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(WindowTitle))] + private string? tasFilePath; + + [RelayCommand(CanExecute = nameof(CanNewFile))] + private void NewFile() { + // Request new file properties + var dialog = m_DialogService.ShowNewFileDialog(); + if (dialog is null) return; + + // Initialize sequence + var seq = new Backend.ListTasSequence(); + // Initialize items + Backend.TasStorage.Init(seq, dialog.Count, dialog.Fps); + // Set members + this.TasFile = seq; + this.TasFilePath = null; + } + + private bool CanNewFile() { + return this.TasFile is null; + } + + [RelayCommand(CanExecute = nameof(CanOpenFile))] + private void OpenFile() { + // Request file path + var dialog = m_DialogService.ShowOpenFileDialog(); + if (dialog is null) return; + + // Initialize sequence + var seq = new Backend.ListTasSequence(); + // Load into sequence + try { + Backend.TasStorage.Load(dialog.Path, seq); + } catch (Exception e) { + m_DialogService.ShowOpenFileFailedDialog(e); + return; + } + // Set members + this.TasFile = seq; + this.TasFilePath = dialog.Path; + } + + private bool CanOpenFile() { + return this.TasFile is null; + } + + [RelayCommand(CanExecute = nameof(CanSaveFile))] + private void SaveFile() { + // If there is no associated file path, + // it means that this file is not stored on disk, + // We must make a request to user for fetching it. + string? filePath = this.TasFilePath; + if (filePath is null) { + var dialog = m_DialogService.ShowSaveFileDialog(); + if (dialog is null) return; + filePath = dialog.Path; + } + + // Save file + try { + Backend.TasStorage.Save(filePath, this.TasFile.Unwrap()); + } catch (Exception e) { + m_DialogService.ShowSaveFileFailedDialog(e); + return; + } + // Update member + this.TasFilePath = filePath; + } + + private bool CanSaveFile() { + return this.TasFile is not null; + } + + [RelayCommand(CanExecute = nameof(CanSaveFileAs))] + private void SaveFileAs() { + // We always request a new path when saving as + var dialog = m_DialogService.ShowSaveFileDialog(); + if (dialog is null) return; + + // Save file + try { + Backend.TasStorage.Save(dialog.Path, this.TasFile.Unwrap()); + } catch (Exception e) { + m_DialogService.ShowSaveFileFailedDialog(e); + return; + } + // Set file path + TasFilePath = dialog.Path; + } + + private bool CanSaveFileAs() { + return this.TasFile is not null; + } + + [RelayCommand(CanExecute = nameof(CanCloseFile))] + private void CloseFile() { + this.TasFile = null; + this.TasFilePath = null; + } + + private bool CanCloseFile() { + return this.TasFile is not null; + } + + #endregion + + #region UI Only + + public string WindowTitle { + get { + if (TasFile is null) { + return "Ballance TAS Editor"; + } else { + if (TasFilePath is null) { + return "Ballance TAS Editor - [Untitled]"; + } else { + return $"Ballance TAS Editor - [{TasFilePath}]"; + } + } + } + } + + #endregion + + } +} diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/NewFileDialog.cs b/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/NewFileDialog.cs index 31bd49f..9f73dc2 100644 --- a/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/NewFileDialog.cs +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/NewFileDialog.cs @@ -9,11 +9,6 @@ using System.Threading.Tasks; namespace BallanceTasEditor.Frontend.ViewModels { - public struct NewFileDialogResult { - public int Count { get; set; } - public float DeltaTime { get; set; } - } - public partial class NewFileDialog : ObservableValidator { public NewFileDialog() { Count = 10000.ToString(); diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/Views/DialogService.cs b/BallanceTasEditor/BallanceTasEditor/Frontend/Views/DialogService.cs new file mode 100644 index 0000000..03d1da3 --- /dev/null +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/Views/DialogService.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace BallanceTasEditor.Frontend.Views { + + public class DialogService : ViewModels.IDialogService { + public DialogService(Window parent) { + m_Parent = parent; + } + + private readonly Window m_Parent; + + public ViewModels.NewFileDialogResult? ShowNewFileDialog() { + var dialog = new NewFileDialog(); + dialog.Owner = m_Parent; + if (dialog.ShowDialog() is true) { + // TODO: Finish result extraction + return new ViewModels.NewFileDialogResult() { Count = 0, Fps = 60 }; + } else { + return null; + } + } + + public ViewModels.OpenFileDialogResult? ShowOpenFileDialog() { + Microsoft.Win32.OpenFileDialog op = new Microsoft.Win32.OpenFileDialog(); + op.RestoreDirectory = true; + op.Multiselect = false; + op.Filter = "TAS file(*.tas)|*.tas|All file(*.*)|*.*"; + if (op.ShowDialog() is true) { + return new ViewModels.OpenFileDialogResult() { Path = op.FileName }; + } else { + return null; + } + } + + public void ShowOpenFileFailedDialog(Exception e) { + MessageBox.Show("Fail to open file. This file might not a legal TAS file." + e.Message, + "Error", + MessageBoxButton.OK, MessageBoxImage.Error); + } + + public ViewModels.SaveFileDialogResult? ShowSaveFileDialog() { + Microsoft.Win32.SaveFileDialog op = new Microsoft.Win32.SaveFileDialog(); + op.RestoreDirectory = true; + op.Filter = "TAS file(*.tas)|*.tas|All file(*.*)|*.*"; + if (op.ShowDialog() is true) { + return new ViewModels.SaveFileDialogResult() { Path = op.FileName }; + } else { + return null; + } + } + + public void ShowSaveFileFailedDialog(Exception e) { + MessageBox.Show( + "Fail to save file." + e.Message, + "Error", + MessageBoxButton.OK, MessageBoxImage.Error); + } + + public bool ShowConfirmCloseFileDialog(string message) { + var rv = MessageBox.Show( + "Do you want to close this TAS file? All changes will not be saved.", + "File Is Not Saved", + MessageBoxButton.YesNo, MessageBoxImage.Warning); + return rv == MessageBoxResult.Yes; + } + + public bool ShowConfirmExitWhenOpeningFileDialog() { + var rv = MessageBox.Show( + "File is not closed. Do you want to just quit? All changes will not be saved.", + "File Is Not Saved", + MessageBoxButton.YesNo, MessageBoxImage.Warning); + return rv == MessageBoxResult.Yes; + } + + public bool ShowFileChangedDialog() { + var rv = MessageBox.Show( + "File is changed. Do you want to reload it?", + "File Is Changed", + MessageBoxButton.YesNo, MessageBoxImage.Question); + return rv == MessageBoxResult.Yes; + } + + public ViewModels.GotoDialogResult? ShowGotoDialog() { + var dialog = new GotoDialog(); + dialog.Owner = m_Parent; + if (dialog.ShowDialog() is true) { + // TODO: Finish result extraction + return new ViewModels.GotoDialogResult(); + } else { + return null; + } + } + + public ViewModels.EditFpsDialogResult? ShowEditFpsDialog() { + var dialog = new EditFpsDialog(); + dialog.Owner = m_Parent; + if (dialog.ShowDialog() is true) { + // TODO: Finish result extraction + return new ViewModels.EditFpsDialogResult(); + } else { + return null; + } + } + + public ViewModels.AddFrameDialogResult? ShowAddFrameDialog() { + var dialog = new AddFrameDialog(); + dialog.Owner = m_Parent; + if (dialog.ShowDialog() is true) { + // TODO: Finish result extraction + return new ViewModels.AddFrameDialogResult(); + } else { + return null; + } + } + + public ViewModels.PreferenceDialogResult? ShowPreferenceDialog() { + var dialog = new PreferenceDialog(); + dialog.Owner = m_Parent; + if (dialog.ShowDialog() is true) { + // TODO: Finish result extraction + return new ViewModels.PreferenceDialogResult(); + } else { + return null; + } + } + + public void ShowAboutDialog() { + var dialog = new AboutDialog(); + dialog.Owner = m_Parent; + dialog.ShowDialog(); + } + } + +} diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/Views/MainWindow.xaml b/BallanceTasEditor/BallanceTasEditor/Frontend/Views/MainWindow.xaml index a367995..83bf885 100644 --- a/BallanceTasEditor/BallanceTasEditor/Frontend/Views/MainWindow.xaml +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/Views/MainWindow.xaml @@ -5,8 +5,11 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:BallanceTasEditor.Frontend.Views" xmlns:styles="clr-namespace:BallanceTasEditor.Frontend.Styles" - mc:Ignorable="d" WindowStartupLocation="CenterScreen" - Title="Ballance TAS Editor" Height="600" Width="800" Icon="/Frontend/Assets/App.ico"> + xmlns:vm="clr-namespace:BallanceTasEditor.Frontend.ViewModels" + 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"> @@ -44,14 +47,14 @@ - - + + - - + + - + @@ -62,20 +65,20 @@ - + - + - + - + @@ -133,14 +136,14 @@ - + - + - + @@ -150,8 +153,6 @@ - - diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/Views/MainWindow.xaml.cs b/BallanceTasEditor/BallanceTasEditor/Frontend/Views/MainWindow.xaml.cs index 62ea1ce..2288607 100644 --- a/BallanceTasEditor/BallanceTasEditor/Frontend/Views/MainWindow.xaml.cs +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/Views/MainWindow.xaml.cs @@ -20,48 +20,8 @@ namespace BallanceTasEditor.Frontend.Views { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); - } - - private void MenuItem_Click_3(object sender, RoutedEventArgs e) { - var dialog = new NewFileDialog(); - dialog.Owner = this; - dialog.ShowDialog(); - } - - private void MenuItem_Click(object sender, RoutedEventArgs e) { - var dialog = new PreferenceDialog(); - dialog.Owner = this; - dialog.ShowDialog(); - } - - private void MenuItem_Click_1(object sender, RoutedEventArgs e) { - var dialog = new AboutDialog(); - dialog.Owner = this; - dialog.ShowDialog(); - } - - private void MenuItem_Click_4(object sender, RoutedEventArgs e) { - var dialog =new GotoDialog(); - dialog.Owner = this; - dialog.ShowDialog(); - } - - private void MenuItem_Click_5(object sender, RoutedEventArgs e) { - var dialog = new EditFpsDialog(); - dialog.Owner = this; - dialog.ShowDialog(); - } - - private void MenuItem_Click_6(object sender, RoutedEventArgs e) { - var dialog = new EditFpsDialog(); - dialog.Owner = this; - dialog.ShowDialog(); - } - - private void MenuItem_Click_2(object sender, RoutedEventArgs e) { - var dialog = new AddFrameDialog(); - dialog.Owner = this; - dialog.ShowDialog(); + var dialogService = new DialogService(this); + this.DataContext = new ViewModels.MainWindow(dialogService); } } diff --git a/BallanceTasEditor/BallanceTasEditor/Backend/NullableExtension.cs b/BallanceTasEditor/BallanceTasEditor/NullableExtension.cs similarity index 92% rename from BallanceTasEditor/BallanceTasEditor/Backend/NullableExtension.cs rename to BallanceTasEditor/BallanceTasEditor/NullableExtension.cs index b2bfd21..ffdb648 100644 --- a/BallanceTasEditor/BallanceTasEditor/Backend/NullableExtension.cs +++ b/BallanceTasEditor/BallanceTasEditor/NullableExtension.cs @@ -5,7 +5,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; -namespace BallanceTasEditor.Backend { +namespace BallanceTasEditor { public static class NullableExtensions { public static T Unwrap(this T? value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : class { ArgumentNullException.ThrowIfNull(value, paramName); diff --git a/BallanceTasEditor/BallanceTasEditor/app.manifest b/BallanceTasEditor/BallanceTasEditor/app.manifest index 9ce67d2..20b4817 100644 --- a/BallanceTasEditor/BallanceTasEditor/app.manifest +++ b/BallanceTasEditor/BallanceTasEditor/app.manifest @@ -40,7 +40,7 @@ - +