From b8f887cd5691d3b6f2e3b71efed968f6994e4ad7 Mon Sep 17 00:00:00 2001 From: Petar Tasev Date: Sun, 18 Jan 2026 21:24:39 -0800 Subject: [PATCH 1/4] Migrate pssg editor from wpf to avalonia --- Directory.Packages.props | 8 + EgoEngineLibrary.slnx | 4 +- .../DialogExtensions.cs | 23 ++ .../EgoEngineLibrary.Avalonia.csproj | 10 + .../FileOpenOptions.cs | 6 + .../FilePickerOptions.cs | 16 ++ .../FilePickerTypes.cs | 34 +++ .../FileSaveOptions.cs | 8 + .../MessageBox/MessageBox.cs | 52 ++++ .../MessageBox/MessageBoxButton.cs | 20 ++ .../MessageBox/MessageBoxImage.cs | 24 ++ .../MessageBox/MessageBoxResult.cs | 26 ++ .../MessageBox/MessageBoxWindow.axaml | 28 ++ .../MessageBox/MessageBoxWindow.axaml.cs | 271 ++++++++++++++++++ src/EgoPssgEditor/{App.xaml => App.axaml} | 0 .../{App.xaml.cs => App.axaml.cs} | 0 .../{Resources => Assets}/Ryder25.ico | Bin .../{Resources => Assets}/disk.png | Bin .../{Resources => Assets}/folder_image.png | Bin .../{Resources => Assets}/image_add.png | Bin src/EgoPssgEditor/EgoPssgEditor.csproj | 41 +-- src/EgoPssgEditor/Models/Interaction.cs | 58 ++++ src/EgoPssgEditor/Program.cs | 32 +++ .../Properties/Settings.Designer.cs | 26 -- .../Properties/Settings.settings | 7 - src/EgoPssgEditor/RelayCommand.cs | 75 ----- src/EgoPssgEditor/ViewLocator.cs | 37 +++ .../CubeMapWorkspaceViewModel.cs | 2 +- .../MainViewModel.cs | 191 +++++------- .../ModelsWorkspaceViewModel.cs | 207 +++++++------ .../NodesWorkspaceViewModel.cs | 180 +++++------- .../PssgAttributeViewModel.cs | 2 +- .../PssgNodeViewModel.cs | 2 +- .../PssgTextureViewModel.cs | 50 ++-- .../TexturesWorkspaceViewModel.cs | 133 ++++----- .../ViewModelBase.cs | 0 .../WorkspaceViewModel.cs | 2 +- .../AddAttributeWindow.axaml} | 6 +- .../AddAttributeWindow.axaml.cs} | 22 +- .../AddNodeWindow.axaml} | 7 +- .../AddNodeWindow.axaml.cs} | 20 +- .../DuplicateTextureWindow.axaml} | 7 +- .../DuplicateTextureWindow.axaml.cs} | 22 +- .../MainWindow.axaml} | 0 .../MainWindow.axaml.cs} | 0 src/EgoPssgEditor/app.manifest | 18 ++ 46 files changed, 1086 insertions(+), 591 deletions(-) create mode 100644 src/EgoEngineLibrary.Avalonia/DialogExtensions.cs create mode 100644 src/EgoEngineLibrary.Avalonia/EgoEngineLibrary.Avalonia.csproj create mode 100644 src/EgoEngineLibrary.Avalonia/FileOpenOptions.cs create mode 100644 src/EgoEngineLibrary.Avalonia/FilePickerOptions.cs create mode 100644 src/EgoEngineLibrary.Avalonia/FilePickerTypes.cs create mode 100644 src/EgoEngineLibrary.Avalonia/FileSaveOptions.cs create mode 100644 src/EgoEngineLibrary.Avalonia/MessageBox/MessageBox.cs create mode 100644 src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxButton.cs create mode 100644 src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxImage.cs create mode 100644 src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxResult.cs create mode 100644 src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxWindow.axaml create mode 100644 src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxWindow.axaml.cs rename src/EgoPssgEditor/{App.xaml => App.axaml} (100%) rename src/EgoPssgEditor/{App.xaml.cs => App.axaml.cs} (100%) rename src/EgoPssgEditor/{Resources => Assets}/Ryder25.ico (100%) rename src/EgoPssgEditor/{Resources => Assets}/disk.png (100%) rename src/EgoPssgEditor/{Resources => Assets}/folder_image.png (100%) rename src/EgoPssgEditor/{Resources => Assets}/image_add.png (100%) create mode 100644 src/EgoPssgEditor/Models/Interaction.cs create mode 100644 src/EgoPssgEditor/Program.cs delete mode 100644 src/EgoPssgEditor/Properties/Settings.Designer.cs delete mode 100644 src/EgoPssgEditor/Properties/Settings.settings delete mode 100644 src/EgoPssgEditor/RelayCommand.cs create mode 100644 src/EgoPssgEditor/ViewLocator.cs rename src/EgoPssgEditor/{ViewModel => ViewModels}/CubeMapWorkspaceViewModel.cs (99%) rename src/EgoPssgEditor/{ViewModel => ViewModels}/MainViewModel.cs (52%) rename src/EgoPssgEditor/{Models3d => ViewModels}/ModelsWorkspaceViewModel.cs (50%) rename src/EgoPssgEditor/{ViewModel => ViewModels}/NodesWorkspaceViewModel.cs (65%) rename src/EgoPssgEditor/{ViewModel => ViewModels}/PssgAttributeViewModel.cs (96%) rename src/EgoPssgEditor/{ViewModel => ViewModels}/PssgNodeViewModel.cs (99%) rename src/EgoPssgEditor/{ViewModel => ViewModels}/PssgTextureViewModel.cs (74%) rename src/EgoPssgEditor/{ViewModel => ViewModels}/TexturesWorkspaceViewModel.cs (66%) rename src/EgoPssgEditor/{ViewModel => ViewModels}/ViewModelBase.cs (100%) rename src/EgoPssgEditor/{ViewModel => ViewModels}/WorkspaceViewModel.cs (93%) rename src/EgoPssgEditor/{AddAttributeWindow.xaml => Views/AddAttributeWindow.axaml} (89%) rename src/EgoPssgEditor/{AddAttributeWindow.xaml.cs => Views/AddAttributeWindow.axaml.cs} (80%) rename src/EgoPssgEditor/{AddNodeWindow.xaml => Views/AddNodeWindow.axaml} (82%) rename src/EgoPssgEditor/{AddNodeWindow.xaml.cs => Views/AddNodeWindow.axaml.cs} (61%) rename src/EgoPssgEditor/{DuplicateTextureWindow.xaml => Views/DuplicateTextureWindow.axaml} (81%) rename src/EgoPssgEditor/{DuplicateTextureWindow.xaml.cs => Views/DuplicateTextureWindow.axaml.cs} (58%) rename src/EgoPssgEditor/{MainWindow.xaml => Views/MainWindow.axaml} (100%) rename src/EgoPssgEditor/{MainWindow.xaml.cs => Views/MainWindow.axaml.cs} (100%) create mode 100644 src/EgoPssgEditor/app.manifest diff --git a/Directory.Packages.props b/Directory.Packages.props index 1c34bea..defcb1b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,15 @@ false + + + + + + + + diff --git a/EgoEngineLibrary.slnx b/EgoEngineLibrary.slnx index 9f4441b..d9f7ee4 100644 --- a/EgoEngineLibrary.slnx +++ b/EgoEngineLibrary.slnx @@ -1,7 +1,4 @@ - - - @@ -14,6 +11,7 @@ + diff --git a/src/EgoEngineLibrary.Avalonia/DialogExtensions.cs b/src/EgoEngineLibrary.Avalonia/DialogExtensions.cs new file mode 100644 index 0000000..7444aed --- /dev/null +++ b/src/EgoEngineLibrary.Avalonia/DialogExtensions.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; + +namespace EgoEngineLibrary.Avalonia; + +public static class DialogExtensions +{ + public static Task ShowDialog(this Window window) + { + return ShowDialog(window); + } + + public static Task ShowDialog(this Window window) + { + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + return window.ShowDialog(desktop.MainWindow!); + } + + throw new InvalidOperationException("Operation not supported on current application lifetime."); + } +} diff --git a/src/EgoEngineLibrary.Avalonia/EgoEngineLibrary.Avalonia.csproj b/src/EgoEngineLibrary.Avalonia/EgoEngineLibrary.Avalonia.csproj new file mode 100644 index 0000000..dd47294 --- /dev/null +++ b/src/EgoEngineLibrary.Avalonia/EgoEngineLibrary.Avalonia.csproj @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/EgoEngineLibrary.Avalonia/FileOpenOptions.cs b/src/EgoEngineLibrary.Avalonia/FileOpenOptions.cs new file mode 100644 index 0000000..c09f57c --- /dev/null +++ b/src/EgoEngineLibrary.Avalonia/FileOpenOptions.cs @@ -0,0 +1,6 @@ +namespace EgoEngineLibrary.Avalonia; + +public record FileOpenOptions : FilePickerOptions +{ + public bool AllowMultiple { get; set; } +} diff --git a/src/EgoEngineLibrary.Avalonia/FilePickerOptions.cs b/src/EgoEngineLibrary.Avalonia/FilePickerOptions.cs new file mode 100644 index 0000000..b14f1a7 --- /dev/null +++ b/src/EgoEngineLibrary.Avalonia/FilePickerOptions.cs @@ -0,0 +1,16 @@ +using Avalonia.Platform.Storage; + +namespace EgoEngineLibrary.Avalonia; + +public record FilePickerOptions +{ + public string? Title { get; set; } + + public string? InitialDirectory { get; set; } + + public string? FileName { get; set; } + + public IReadOnlyList? FileTypeChoices { get; set; } + + public FilePickerFileType? SuggestedFileType { get; set; } +} diff --git a/src/EgoEngineLibrary.Avalonia/FilePickerTypes.cs b/src/EgoEngineLibrary.Avalonia/FilePickerTypes.cs new file mode 100644 index 0000000..e60d22a --- /dev/null +++ b/src/EgoEngineLibrary.Avalonia/FilePickerTypes.cs @@ -0,0 +1,34 @@ +using Avalonia.Platform.Storage; + +namespace EgoEngineLibrary.Avalonia; + +public static class FilePickerTypes +{ + public static readonly FilePickerFileType Pssg = new("PSSG files") + { + Patterns = ["*.pssg"], + MimeTypes = ["application/octet-stream"], + AppleUniformTypeIdentifiers = ["public.data"] + }; + + public static readonly FilePickerFileType Dds = new("DDS files") + { + Patterns = ["*.dds"], + MimeTypes = ["application/octet-stream"], + AppleUniformTypeIdentifiers = ["public.image"] + }; + + public static readonly FilePickerFileType Gltf = new("Gltf files") + { + Patterns = ["*.glb", "*.gltf"], + MimeTypes = ["model/gltf-binary", "model/gltf+json"], + AppleUniformTypeIdentifiers = ["public.3d-content"] + }; + + public static readonly FilePickerFileType Bin = new("Bin files") + { + Patterns = ["*.bin"], + MimeTypes = ["application/octet-stream"], + AppleUniformTypeIdentifiers = ["public.data"] + }; +} diff --git a/src/EgoEngineLibrary.Avalonia/FileSaveOptions.cs b/src/EgoEngineLibrary.Avalonia/FileSaveOptions.cs new file mode 100644 index 0000000..07c18bb --- /dev/null +++ b/src/EgoEngineLibrary.Avalonia/FileSaveOptions.cs @@ -0,0 +1,8 @@ +namespace EgoEngineLibrary.Avalonia; + +public record FileSaveOptions : FilePickerOptions +{ + public string? DefaultExtension { get; set; } + + public bool? ShowOverwritePrompt { get; set; } = true; +} diff --git a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBox.cs b/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBox.cs new file mode 100644 index 0000000..75de821 --- /dev/null +++ b/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBox.cs @@ -0,0 +1,52 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; + +namespace EgoEngineLibrary.Avalonia.MessageBox; + +public sealed class MessageBox +{ + public static Task Show( + string messageBoxText, + string caption, + MessageBoxButton button = MessageBoxButton.OK, + MessageBoxImage icon = MessageBoxImage.None, + MessageBoxResult defaultResult = MessageBoxResult.None) + { + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + return Show(desktop.MainWindow!, messageBoxText, caption, button, icon, defaultResult); + } + else if (Application.Current?.ApplicationLifetime is null) + { + return Task.FromResult(MessageBoxResult.None); + } + + throw new InvalidOperationException("Operation not supported on current application lifetime."); + } + + public static Task Show( + Window owner, + string messageBoxText, + string caption, + MessageBoxButton button = MessageBoxButton.OK, + MessageBoxImage icon = MessageBoxImage.None, + MessageBoxResult defaultResult = MessageBoxResult.None) + { + ArgumentNullException.ThrowIfNull(owner); + + MessageBoxWindow box = new() + { + MinWidth = 114, + MinHeight = 94, + MaxWidth = owner.Width, + MaxHeight = owner.Height, + Title = caption, + Message = messageBoxText, + Buttons = button, + ImageIcon = icon, + DefaultResult = defaultResult + }; + return box.ShowDialog(); + } +} diff --git a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxButton.cs b/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxButton.cs new file mode 100644 index 0000000..b590f1b --- /dev/null +++ b/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxButton.cs @@ -0,0 +1,20 @@ +namespace EgoEngineLibrary.Avalonia.MessageBox; + +/// Specifies the buttons that are displayed on a message box. +public enum MessageBoxButton +{ + /// The message box displays an OK button. + OK, + /// The message box displays OK and Cancel buttons. + OKCancel, + /// The message box displays Abort, Retry, and Ignore buttons. + AbortRetryIgnore, + /// The message box displays Yes, No, and Cancel buttons. + YesNoCancel, + /// The message box displays Yes and No buttons. + YesNo, + /// The message box displays Retry and Cancel** buttons. + RetryCancel, + /// The message box displays Cancel, Retry, and Continue buttons. + CancelTryContinue, +} diff --git a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxImage.cs b/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxImage.cs new file mode 100644 index 0000000..4157a47 --- /dev/null +++ b/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxImage.cs @@ -0,0 +1,24 @@ +namespace EgoEngineLibrary.Avalonia.MessageBox; + +/// Specifies the icon that is displayed by a message box. +public enum MessageBoxImage +{ + /// The message box contains no symbols. + None = 0, + /// The message box contains a symbol consisting of white X in a circle with a red background. + Error = 16, + /// The message box contains a symbol consisting of a white X in a circle with a red background. + Hand = 16, + /// The message box contains a symbol consisting of white X in a circle with a red background. + Stop = 16, + /// The message box contains a symbol consisting of a question mark in a circle. The question mark message icon is no longer recommended because it does not clearly represent a specific type of message and because the phrasing of a message as a question could apply to any message type. In addition, users can confuse the question mark symbol with a help information symbol. Therefore, do not use this question mark symbol in your message boxes. The system continues to support its inclusion only for backward compatibility. + Question = 32, + /// The message box contains a symbol consisting of an exclamation point in a triangle with a yellow background. + Exclamation = 48, + /// The message box contains a symbol consisting of an exclamation point in a triangle with a yellow background. + Warning = 48, + /// The message box contains a symbol consisting of a lowercase letter i in a circle. + Asterisk = 64, + /// The message box contains a symbol consisting of a lowercase letter i in a circle. + Information = 64, +} diff --git a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxResult.cs b/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxResult.cs new file mode 100644 index 0000000..c950659 --- /dev/null +++ b/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxResult.cs @@ -0,0 +1,26 @@ +namespace EgoEngineLibrary.Avalonia.MessageBox; + +/// Specifies which message box button that a user clicks. +public enum MessageBoxResult +{ + /// The message box returns no result. + None = 0, + /// The result value of the message box is OK. + OK = 1, + /// The result value of the message box is Cancel. + Cancel = 2, + /// The result value of the message box is Abort (usually sent from a button labeled Abort). + Abort = 3, + /// The result value of the message box is Retry (usually sent from a button labeled Retry). + Retry = 4, + /// The result value of the message box is Ignore (usually sent from a button labeled Ignore). + Ignore = 5, + /// The result value of the message box is Yes. + Yes = 6, + /// The result value of the message box is No. + No = 7, + /// The result value of the message box is TryAgain (usually sent from a button labeled Try Again). + TryAgain = 10, + /// The result value of the message box is Continue (usually sent from a button labeled Continue). + Continue = 11 +} diff --git a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxWindow.axaml b/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxWindow.axaml new file mode 100644 index 0000000..eb67e6a --- /dev/null +++ b/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxWindow.axaml @@ -0,0 +1,28 @@ + + + M12,2 C17.523,2 22,6.478 22,12 C22,17.522 17.523,22 12,22 C6.477,22 2,17.522 2,12 C2,6.478 6.477,2 12,2 Z M12,3.667 C7.405,3.667 3.667,7.405 3.667,12 C3.667,16.595 7.405,20.333 12,20.333 C16.595,20.333 20.333,16.595 20.333,12 C20.333,7.405 16.595,3.667 12,3.667 Z M11.9986626,14.5022358 C12.5502088,14.5022358 12.9973253,14.9493523 12.9973253,15.5008984 C12.9973253,16.0524446 12.5502088,16.4995611 11.9986626,16.4995611 C11.4471165,16.4995611 11,16.0524446 11,15.5008984 C11,14.9493523 11.4471165,14.5022358 11.9986626,14.5022358 Z M11.9944624,7 C12.3741581,6.99969679 12.6881788,7.28159963 12.7381342,7.64763535 L12.745062,7.7494004 L12.7486629,12.2509944 C12.7489937,12.6652079 12.4134759,13.0012627 11.9992625,13.0015945 C11.6195668,13.0018977 11.3055461,12.7199949 11.2555909,12.3539592 L11.2486629,12.2521941 L11.245062,7.7506001 C11.2447312,7.33638667 11.580249,7.00033178 11.9944624,7 Z + M14,2 C20.6274,2 26,7.37258 26,14 C26,20.6274 20.6274,26 14,26 C7.37258,26 2,20.6274 2,14 C2,7.37258 7.37258,2 14,2 Z M14,3.5 C8.20101,3.5 3.5,8.20101 3.5,14 C3.5,19.799 8.20101,24.5 14,24.5 C19.799,24.5 24.5,19.799 24.5,14 C24.5,8.20101 19.799,3.5 14,3.5 Z M14,11 C14.3796833,11 14.6934889,11.2821653 14.7431531,11.6482323 L14.75,11.75 L14.75,19.25 C14.75,19.6642 14.4142,20 14,20 C13.6203167,20 13.3065111,19.7178347 13.2568469,19.3517677 L13.25,19.25 L13.25,11.75 C13.25,11.3358 13.5858,11 14,11 Z M14,7 C14.5523,7 15,7.44772 15,8 C15,8.55228 14.5523,9 14,9 C13.4477,9 13,8.55228 13,8 C13,7.44772 13.4477,7 14,7 Z + M24 4C35.0457 4 44 12.9543 44 24C44 35.0457 35.0457 44 24 44C12.9543 44 4 35.0457 4 24C4 12.9543 12.9543 4 24 4ZM24 6.5C14.335 6.5 6.5 14.335 6.5 24C6.5 33.665 14.335 41.5 24 41.5C33.665 41.5 41.5 33.665 41.5 24C41.5 14.335 33.665 6.5 24 6.5ZM24.25 32C25.0784 32 25.75 32.6716 25.75 33.5C25.75 34.3284 25.0784 35 24.25 35C23.4216 35 22.75 34.3284 22.75 33.5C22.75 32.6716 23.4216 32 24.25 32ZM24.25 13C27.6147 13 30.5 15.8821 30.5 19.2488C30.502 21.3691 29.7314 22.7192 27.8216 24.7772L26.8066 25.8638C25.7842 27.0028 25.3794 27.7252 25.3409 28.5793L25.3379 28.7411L25.3323 28.8689L25.3143 28.9932C25.2018 29.5636 24.7009 29.9957 24.0968 30.0001C23.4065 30.0049 22.8428 29.4493 22.8379 28.7589C22.8251 26.9703 23.5147 25.7467 25.1461 23.9739L26.1734 22.8762C27.5312 21.3837 28.0012 20.503 28 19.25C28 17.2634 26.2346 15.5 24.25 15.5C22.3307 15.5 20.6142 17.1536 20.5055 19.0587L20.4935 19.3778C20.4295 20.0081 19.8972 20.5 19.25 20.5C18.5596 20.5 18 19.9404 18 19.25C18 15.8846 20.8864 13 24.25 13Z + M10.9093922,2.78216375 C11.9491636,2.20625071 13.2471955,2.54089334 13.8850247,3.52240345 L13.9678229,3.66023048 L21.7267791,17.6684928 C21.9115773,18.0021332 22.0085303,18.3772743 22.0085303,18.7586748 C22.0085303,19.9495388 21.0833687,20.9243197 19.9125791,21.003484 L19.7585303,21.0086748 L4.24277801,21.0086748 C3.86146742,21.0086748 3.48641186,20.9117674 3.15282824,20.7270522 C2.11298886,20.1512618 1.7079483,18.8734454 2.20150311,17.8120352 L2.27440063,17.668725 L10.0311968,3.66046274 C10.2357246,3.291099 10.5400526,2.98673515 10.9093922,2.78216375 Z M20.4146132,18.3952808 L12.6556571,4.3870185 C12.4549601,4.02467391 11.9985248,3.89363262 11.6361802,4.09432959 C11.5438453,4.14547244 11.4637001,4.21532637 11.4006367,4.29899869 L11.3434484,4.38709592 L3.58665221,18.3953582 C3.385998,18.7577265 3.51709315,19.2141464 3.87946142,19.4148006 C3.96285732,19.4609794 4.05402922,19.4906942 4.14802472,19.5026655 L4.24277801,19.5086748 L19.7585303,19.5086748 C20.1727439,19.5086748 20.5085303,19.1728883 20.5085303,18.7586748 C20.5085303,18.6633247 20.4903516,18.5691482 20.455275,18.4811011 L20.4146132,18.3952808 L12.6556571,4.3870185 L20.4146132,18.3952808 Z M12.0004478,16.0017852 C12.5519939,16.0017852 12.9991104,16.4489016 12.9991104,17.0004478 C12.9991104,17.5519939 12.5519939,17.9991104 12.0004478,17.9991104 C11.4489016,17.9991104 11.0017852,17.5519939 11.0017852,17.0004478 C11.0017852,16.4489016 11.4489016,16.0017852 12.0004478,16.0017852 Z M11.9962476,8.49954934 C12.3759432,8.49924613 12.689964,8.78114897 12.7399193,9.14718469 L12.7468472,9.24894974 L12.750448,13.7505438 C12.7507788,14.1647572 12.4152611,14.5008121 12.0010476,14.5011439 C11.621352,14.5014471 11.3073312,14.2195442 11.257376,13.8535085 L11.250448,13.7517435 L11.2468472,9.25014944 C11.2465164,8.83593601 11.5820341,8.49988112 11.9962476,8.49954934 Z + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxWindow.axaml.cs b/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxWindow.axaml.cs new file mode 100644 index 0000000..adbe3c5 --- /dev/null +++ b/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxWindow.axaml.cs @@ -0,0 +1,271 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace EgoEngineLibrary.Avalonia.MessageBox; + +internal partial class MessageBoxWindow : Window +{ + private bool _closeByButton; + + public string? Message + { + get => MessageTextBlock.Text; + set => MessageTextBlock.Text = value; + } + + public MessageBoxResult DefaultResult + { + get; + set + { + field = value; + SetDefaultButton(); + } + } + + public MessageBoxButton Buttons + { + get; + set + { + field = value; + Button cancelButton; + switch (value) + { + case MessageBoxButton.OK: + FirstButton.Content = "Ok"; + SecondButton.IsVisible = false; + ThirdButton.IsVisible = false; + cancelButton = FirstButton; + break; + case MessageBoxButton.OKCancel: + FirstButton.Content = "Ok"; + SecondButton.Content = "Cancel"; + ThirdButton.IsVisible = false; + cancelButton = SecondButton; + break; + case MessageBoxButton.AbortRetryIgnore: + FirstButton.Content = "Abort"; + SecondButton.Content = "Retry"; + ThirdButton.Content = "Ignore"; + cancelButton = FirstButton; + break; + case MessageBoxButton.YesNoCancel: + FirstButton.Content = "Yes"; + SecondButton.Content = "No"; + ThirdButton.Content = "Cancel"; + cancelButton = ThirdButton; + break; + case MessageBoxButton.YesNo: + FirstButton.Content = "Yes"; + SecondButton.Content = "No"; + ThirdButton.IsVisible = false; + cancelButton = SecondButton; + break; + case MessageBoxButton.RetryCancel: + FirstButton.Content = "Retry"; + SecondButton.Content = "Cancel"; + ThirdButton.IsVisible = false; + cancelButton = SecondButton; + break; + case MessageBoxButton.CancelTryContinue: + FirstButton.Content = "Cancel"; + SecondButton.Content = "Try Again"; + ThirdButton.Content = "Continue"; + cancelButton = FirstButton; + break; + default: + throw new ArgumentOutOfRangeException(nameof(Buttons), value, null); + } + + FirstButton.IsCancel = false; + SecondButton.IsCancel = false; + ThirdButton.IsCancel = false; + cancelButton.IsCancel = true; + + SetDefaultButton(); + } + } + + public MessageBoxImage ImageIcon + { + get; + set + { + field = value; + Image.IsVisible = true; + switch (value) + { + case MessageBoxImage.None: + Image.Data = null; + Image.IsVisible = false; + break; + case MessageBoxImage.Error: + Image.Data = this.FindResource(this.ActualThemeVariant, "ErrorCircleRegular") as Geometry; + Image.Foreground = Brushes.Firebrick; + break; + case MessageBoxImage.Question: + Image.Data = this.FindResource(this.ActualThemeVariant, "QuestionCircleRegular") as Geometry; + Image.Foreground = Brushes.SteelBlue; + break; + case MessageBoxImage.Exclamation: + Image.Data = this.FindResource(this.ActualThemeVariant, "WarningRegular") as Geometry; + Image.Foreground = Brushes.Goldenrod; + break; + case MessageBoxImage.Asterisk: + Image.Data = this.FindResource(this.ActualThemeVariant, "InfoRegular") as Geometry; + Image.Foreground = Brushes.SteelBlue; + break; + default: + throw new ArgumentOutOfRangeException(nameof(ImageIcon), value, null); + } + } + } + + public MessageBoxWindow() + { + InitializeComponent(); + Buttons = MessageBoxButton.OK; + ImageIcon = MessageBoxImage.None; + DefaultResult = MessageBoxResult.None; + } + + private void SetDefaultButton() + { + FirstButton.IsDefault = false; + SecondButton.IsDefault = false; + ThirdButton.IsDefault = false; + + if (DefaultResult == MessageBoxResult.None) + { + FirstButton.IsDefault = true; + return; + } + + var defaultButton = Buttons switch + { + MessageBoxButton.OK => FirstButton, + MessageBoxButton.OKCancel => DefaultResult is MessageBoxResult.Cancel ? SecondButton : FirstButton, + MessageBoxButton.AbortRetryIgnore => DefaultResult switch + { + MessageBoxResult.Retry => SecondButton, + MessageBoxResult.Ignore => ThirdButton, + _ => FirstButton + }, + MessageBoxButton.YesNoCancel => DefaultResult switch + { + MessageBoxResult.No => SecondButton, + MessageBoxResult.Cancel => ThirdButton, + _ => FirstButton + }, + MessageBoxButton.YesNo => DefaultResult is MessageBoxResult.No ? SecondButton : FirstButton, + MessageBoxButton.RetryCancel => DefaultResult is MessageBoxResult.Cancel ? SecondButton : FirstButton, + MessageBoxButton.CancelTryContinue => DefaultResult switch + { + MessageBoxResult.TryAgain => SecondButton, + MessageBoxResult.Continue => ThirdButton, + _ => FirstButton + }, + _ => FirstButton + }; + + defaultButton.IsDefault = true; + } + + private void FirstButton_OnClick(object? sender, RoutedEventArgs e) + { + _closeByButton = true; + switch (Buttons) + { + case MessageBoxButton.OK: + Close(MessageBoxResult.OK); + break; + case MessageBoxButton.OKCancel: + Close(MessageBoxResult.OK); + break; + case MessageBoxButton.AbortRetryIgnore: + Close(MessageBoxResult.Abort); + break; + case MessageBoxButton.YesNoCancel: + Close(MessageBoxResult.Yes); + break; + case MessageBoxButton.YesNo: + Close(MessageBoxResult.Yes); + break; + case MessageBoxButton.RetryCancel: + Close(MessageBoxResult.Retry); + break; + case MessageBoxButton.CancelTryContinue: + Close(MessageBoxResult.Cancel); + break; + default: + throw new InvalidOperationException("First button is not supported."); + } + } + + private void SecondButton_OnClick(object? sender, RoutedEventArgs e) + { + _closeByButton = true; + switch (Buttons) + { + case MessageBoxButton.OKCancel: + Close(MessageBoxResult.Cancel); + break; + case MessageBoxButton.AbortRetryIgnore: + Close(MessageBoxResult.Retry); + break; + case MessageBoxButton.YesNoCancel: + Close(MessageBoxResult.No); + break; + case MessageBoxButton.YesNo: + Close(MessageBoxResult.No); + break; + case MessageBoxButton.RetryCancel: + Close(MessageBoxResult.Cancel); + break; + case MessageBoxButton.CancelTryContinue: + Close(MessageBoxResult.TryAgain); + break; + case MessageBoxButton.OK: + default: + throw new InvalidOperationException("Second button is not supported."); + } + } + + private void ThirdButton_OnClick(object? sender, RoutedEventArgs e) + { + _closeByButton = true; + switch (Buttons) + { + case MessageBoxButton.AbortRetryIgnore: + Close(MessageBoxResult.Ignore); + break; + case MessageBoxButton.YesNoCancel: + Close(MessageBoxResult.Cancel); + break; + case MessageBoxButton.CancelTryContinue: + Close(MessageBoxResult.Continue); + break; + case MessageBoxButton.OK: + case MessageBoxButton.OKCancel: + case MessageBoxButton.YesNo: + case MessageBoxButton.RetryCancel: + default: + throw new InvalidOperationException("Third button is not supported."); + } + } + + protected override void OnClosing(WindowClosingEventArgs e) + { + if (_closeByButton) + { + base.OnClosing(e); + return; + } + + e.Cancel = true; + var cancelButton = SecondButton.IsCancel ? SecondButton : (ThirdButton.IsCancel ? ThirdButton : FirstButton); + cancelButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); + } +} diff --git a/src/EgoPssgEditor/App.xaml b/src/EgoPssgEditor/App.axaml similarity index 100% rename from src/EgoPssgEditor/App.xaml rename to src/EgoPssgEditor/App.axaml diff --git a/src/EgoPssgEditor/App.xaml.cs b/src/EgoPssgEditor/App.axaml.cs similarity index 100% rename from src/EgoPssgEditor/App.xaml.cs rename to src/EgoPssgEditor/App.axaml.cs diff --git a/src/EgoPssgEditor/Resources/Ryder25.ico b/src/EgoPssgEditor/Assets/Ryder25.ico similarity index 100% rename from src/EgoPssgEditor/Resources/Ryder25.ico rename to src/EgoPssgEditor/Assets/Ryder25.ico diff --git a/src/EgoPssgEditor/Resources/disk.png b/src/EgoPssgEditor/Assets/disk.png similarity index 100% rename from src/EgoPssgEditor/Resources/disk.png rename to src/EgoPssgEditor/Assets/disk.png diff --git a/src/EgoPssgEditor/Resources/folder_image.png b/src/EgoPssgEditor/Assets/folder_image.png similarity index 100% rename from src/EgoPssgEditor/Resources/folder_image.png rename to src/EgoPssgEditor/Assets/folder_image.png diff --git a/src/EgoPssgEditor/Resources/image_add.png b/src/EgoPssgEditor/Assets/image_add.png similarity index 100% rename from src/EgoPssgEditor/Resources/image_add.png rename to src/EgoPssgEditor/Assets/image_add.png diff --git a/src/EgoPssgEditor/EgoPssgEditor.csproj b/src/EgoPssgEditor/EgoPssgEditor.csproj index 491a382..baf361c 100644 --- a/src/EgoPssgEditor/EgoPssgEditor.csproj +++ b/src/EgoPssgEditor/EgoPssgEditor.csproj @@ -1,13 +1,14 @@  - $(TargetFramework)-windows WinExe - true Ego PSSG Editor + app.manifest + true + true - Resources\Ryder25.ico + Assets\Ryder25.ico Copyright © Petar Tasev 2010 - 2024 Petar Tasev Petar Tasev @@ -34,27 +35,33 @@ - - - + + + + - + - - True - True - Settings.settings - + + + + + + + + + None + All + + + - - SettingsSingleFileGenerator - Settings.Designer.cs - + @@ -71,4 +78,4 @@ Resources.Designer.cs - \ No newline at end of file + diff --git a/src/EgoPssgEditor/Models/Interaction.cs b/src/EgoPssgEditor/Models/Interaction.cs new file mode 100644 index 0000000..a611af4 --- /dev/null +++ b/src/EgoPssgEditor/Models/Interaction.cs @@ -0,0 +1,58 @@ +using System.Windows.Input; + +namespace EgoPssgEditor.Models; + +/// +/// Simple implementation of Interaction pattern from ReactiveUI framework. +/// https://www.reactiveui.net/docs/handbook/interactions/ +/// +public sealed class Interaction : IDisposable, ICommand +{ + // this is a reference to the registered interaction handler. + private Func>? _handler; + + /// + /// Performs the requested interaction . Returns the result provided by the View + /// + /// The input parameter + /// The result of the interaction + /// + public Task HandleAsync(TInput input) + { + if (_handler is null) + { + throw new InvalidOperationException("Handler wasn't registered"); + } + + return _handler(input); + } + + /// + /// Registers a handler to our Interaction + /// + /// the handler to register + /// a disposable object to clean up memory if not in use anymore/> + /// + public IDisposable RegisterHandler(Func> handler) + { + if (_handler is not null) + { + throw new InvalidOperationException("Handler was already registered"); + } + + _handler = handler; + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + return this; + } + + public void Dispose() + { + _handler = null; + } + + public bool CanExecute(object? parameter) => _handler is not null; + + public void Execute(object? parameter) => HandleAsync((TInput?)parameter!); + + public event EventHandler? CanExecuteChanged; +} diff --git a/src/EgoPssgEditor/Program.cs b/src/EgoPssgEditor/Program.cs new file mode 100644 index 0000000..6333438 --- /dev/null +++ b/src/EgoPssgEditor/Program.cs @@ -0,0 +1,32 @@ +using Avalonia; +using System; +using System.Globalization; + +namespace EgoPssgEditor; + +sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) + { + var culture = new CultureInfo("en-US"); + + Thread.CurrentThread.CurrentCulture = culture; + Thread.CurrentThread.CurrentUICulture = culture; + CultureInfo.DefaultThreadCurrentCulture = culture; + CultureInfo.DefaultThreadCurrentUICulture = culture; + + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} diff --git a/src/EgoPssgEditor/Properties/Settings.Designer.cs b/src/EgoPssgEditor/Properties/Settings.Designer.cs deleted file mode 100644 index 403b46c..0000000 --- a/src/EgoPssgEditor/Properties/Settings.Designer.cs +++ /dev/null @@ -1,26 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace EgoPssgEditor.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} diff --git a/src/EgoPssgEditor/Properties/Settings.settings b/src/EgoPssgEditor/Properties/Settings.settings deleted file mode 100644 index 033d7a5..0000000 --- a/src/EgoPssgEditor/Properties/Settings.settings +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/EgoPssgEditor/RelayCommand.cs b/src/EgoPssgEditor/RelayCommand.cs deleted file mode 100644 index e575e30..0000000 --- a/src/EgoPssgEditor/RelayCommand.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Input; - -namespace EgoPssgEditor -{ - /// - /// A command whose sole purpose is to - /// relay its functionality to other - /// objects by invoking delegates. The - /// default return value for the CanExecute - /// method is 'true'. - /// - public class RelayCommand : ICommand - { - #region Fields - - readonly Action _execute; - readonly Predicate _canExecute; - - #endregion // Fields - - #region Constructors - - /// - /// Creates a new command that can always execute. - /// - /// The execution logic. - public RelayCommand(Action execute) - : this(execute, null) - { - } - - /// - /// Creates a new command. - /// - /// The execution logic. - /// The execution status logic. - public RelayCommand(Action execute, Predicate canExecute) - { - if (execute == null) - throw new ArgumentNullException("execute"); - - _execute = execute; - _canExecute = canExecute; - } - - #endregion // Constructors - - #region ICommand Members - - [DebuggerStepThrough] - public bool CanExecute(object parameter) - { - return _canExecute == null ? true : _canExecute(parameter); - } - - public event EventHandler CanExecuteChanged - { - add { CommandManager.RequerySuggested += value; } - remove { CommandManager.RequerySuggested -= value; } - } - - public void Execute(object parameter) - { - _execute(parameter); - } - - #endregion // ICommand Members - } -} diff --git a/src/EgoPssgEditor/ViewLocator.cs b/src/EgoPssgEditor/ViewLocator.cs new file mode 100644 index 0000000..f1990c9 --- /dev/null +++ b/src/EgoPssgEditor/ViewLocator.cs @@ -0,0 +1,37 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using EgoPssgEditor.ViewModels; + +namespace EgoPssgEditor; + +/// +/// Given a view model, returns the corresponding view if possible. +/// +[RequiresUnreferencedCode( + "Default implementation of ViewLocator involves reflection which may be trimmed away.", + Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")] +public class ViewLocator : IDataTemplate +{ + public Control? Build(object? param) + { + if (param is null) + return null; + + var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } +} diff --git a/src/EgoPssgEditor/ViewModel/CubeMapWorkspaceViewModel.cs b/src/EgoPssgEditor/ViewModels/CubeMapWorkspaceViewModel.cs similarity index 99% rename from src/EgoPssgEditor/ViewModel/CubeMapWorkspaceViewModel.cs rename to src/EgoPssgEditor/ViewModels/CubeMapWorkspaceViewModel.cs index 430523b..9acbeec 100644 --- a/src/EgoPssgEditor/ViewModel/CubeMapWorkspaceViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/CubeMapWorkspaceViewModel.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace EgoPssgEditor.ViewModel +namespace EgoPssgEditor.ViewModels { public class CubeMapWorkspaceViewModel : WorkspaceViewModel { diff --git a/src/EgoPssgEditor/ViewModel/MainViewModel.cs b/src/EgoPssgEditor/ViewModels/MainViewModel.cs similarity index 52% rename from src/EgoPssgEditor/ViewModel/MainViewModel.cs rename to src/EgoPssgEditor/ViewModels/MainViewModel.cs index 48c2b15..b66a724 100644 --- a/src/EgoPssgEditor/ViewModel/MainViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/MainViewModel.cs @@ -1,14 +1,24 @@ using EgoEngineLibrary.Graphics; -using EgoPssgEditor.Models3d; using Microsoft.Win32; using System; using System.IO; using System.Threading.Tasks; using System.Windows; -namespace EgoPssgEditor.ViewModel +using Avalonia.Controls; +using Avalonia.Platform.Storage; + +using CommunityToolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.Input; + +using EgoEngineLibrary.Avalonia; +using EgoEngineLibrary.Avalonia.MessageBox; + +using EgoPssgEditor.Models; + +namespace EgoPssgEditor.ViewModels { - public class MainViewModel : ViewModelBase + public partial class MainViewModel : ViewModelBase { #region Data readonly string schemaPath; @@ -65,6 +75,9 @@ public int SelectedTabIndex } #endregion + public Interaction FileOpenInteraction { get; } = new(); + public Interaction FileSaveInteraction { get; } = new(); + public MainViewModel() { @@ -75,70 +88,13 @@ public MainViewModel() texturesWorkspace = new TexturesWorkspaceViewModel(this); _modelsWorkspace = new ModelsWorkspaceViewModel(this); - // Commands - newCommand = new RelayCommand(NewCommand_Execute); - openCommand = new RelayCommand(OpenCommand_Execute); - saveCommand = new RelayCommand(SaveCommand_Execute, SaveCommand_CanExecute); - savePssgCommand = new RelayCommand(SavePssgCommand_Execute, SaveCommand_CanExecute); - saveCompressedCommand = new RelayCommand(SaveCompressedCommand_Execute, SaveCommand_CanExecute); - saveXmlCommand = new RelayCommand(SaveXmlCommand_Execute, SaveCommand_CanExecute); - loadSchema = new RelayCommand(LoadSchema_Execute); - saveSchema = new RelayCommand(SaveSchema_Execute); - clearSchema = new RelayCommand(ClearSchema_Execute); - - try { LoadSchema_Execute(null); } catch { } + try { LoadSchema(); } catch { } ParseCommandLineArguments(); } #region MainMenu - readonly RelayCommand newCommand; - readonly RelayCommand openCommand; - readonly RelayCommand saveCommand; - readonly RelayCommand savePssgCommand; - readonly RelayCommand saveCompressedCommand; - readonly RelayCommand saveXmlCommand; - readonly RelayCommand loadSchema; - readonly RelayCommand saveSchema; - readonly RelayCommand clearSchema; - - public RelayCommand NewCommand - { - get { return newCommand; } - } - public RelayCommand OpenCommand - { - get { return openCommand; } - } - public RelayCommand SaveCommand - { - get { return saveCommand; } - } - public RelayCommand SavePssgCommand - { - get { return savePssgCommand; } - } - public RelayCommand SaveCompressedCommand - { - get { return saveCompressedCommand; } - } - public RelayCommand SaveXmlCommand - { - get { return saveXmlCommand; } - } - public RelayCommand LoadSchema - { - get { return loadSchema; } - } - public RelayCommand SaveSchema - { - get { return saveSchema; } - } - public RelayCommand ClearSchema - { - get { return clearSchema; } - } - public void ParseCommandLineArguments() + public async void ParseCommandLineArguments() { string[] args = Environment.GetCommandLineArgs(); @@ -158,33 +114,39 @@ public void ParseCommandLineArguments() { // Fail DisplayName = Properties.Resources.AppTitleLong; - MessageBox.Show("The program could not open this file!" + Environment.NewLine + Environment.NewLine + excp.Message, "Could Not Open", MessageBoxButton.OK, MessageBoxImage.Error); + await MessageBox.Show("The program could not open this file!" + Environment.NewLine + Environment.NewLine + excp.Message, "Could Not Open", MessageBoxButton.OK, MessageBoxImage.Error); } } } - private void NewCommand_Execute(object parameter) + [RelayCommand] + private void New() { ClearVars(true); file = new PssgFile(PssgFileType.Pssg); SaveTag(); } - private void OpenCommand_Execute(object parameter) + [RelayCommand] + private async Task Open() { - OpenFileDialog openFileDialog = new OpenFileDialog(); - openFileDialog.Filter = "PSSG files|*.pssg|DDS files|*.dds|Xml files|*.xml|All files|*.*"; - openFileDialog.FilterIndex = 1; + FileOpenOptions openOptions = new() + { + Title = "Open pssg", + FileTypeChoices = [FilePickerTypes.Pssg, FilePickerFileTypes.Xml, FilePickerFileTypes.All], + SuggestedFileType = FilePickerTypes.Pssg, + }; if (!string.IsNullOrEmpty(filePath)) { - openFileDialog.FileName = Path.GetFileNameWithoutExtension(filePath); - openFileDialog.InitialDirectory = Path.GetDirectoryName(filePath); + openOptions.FileName = Path.GetFileNameWithoutExtension(filePath); + openOptions.InitialDirectory = Path.GetDirectoryName(filePath); } - if (openFileDialog.ShowDialog() == true) + var result = await FileOpenInteraction.HandleAsync(openOptions); + if (result is not null) { try { - filePath = openFileDialog.FileName; + filePath = result; using (var fs = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { var pssg = PssgFile.Open(fs); @@ -196,39 +158,46 @@ private void OpenCommand_Execute(object parameter) { // Fail DisplayName = Properties.Resources.AppTitleLong; - MessageBox.Show("The program could not open this file!" + Environment.NewLine + Environment.NewLine + excp.Message, "Could Not Open", MessageBoxButton.OK, MessageBoxImage.Error); + await MessageBox.Show("The program could not open this file!" + Environment.NewLine + Environment.NewLine + excp.Message, "Could Not Open", MessageBoxButton.OK, MessageBoxImage.Error); } } } - private bool SaveCommand_CanExecute(object parameter) + private bool SaveCommand_CanExecute() { return file != null; } - private void SaveCommand_Execute(object parameter) + [RelayCommand(CanExecute = nameof(SaveCommand_CanExecute))] + private Task Save() { - SavePssg(0); + return SavePssg(file.FileType); } - private void SavePssgCommand_Execute(object parameter) + [RelayCommand(CanExecute = nameof(SaveCommand_CanExecute))] + private Task SavePssg() { - SavePssg(1); + return SavePssg(PssgFileType.Pssg); } - private void SaveCompressedCommand_Execute(object parameter) + [RelayCommand(CanExecute = nameof(SaveCommand_CanExecute))] + private Task SaveCompressed() { - SavePssg(2); + return SavePssg(PssgFileType.CompressedPssg); } - private void SaveXmlCommand_Execute(object parameter) + [RelayCommand(CanExecute = nameof(SaveCommand_CanExecute))] + private Task SaveXml() { - SavePssg(3); + return SavePssg(PssgFileType.Xml); } - private void LoadSchema_Execute(object parameter) + [RelayCommand] + private void LoadSchema() { PssgSchema.LoadSchema(File.Open(schemaPath, FileMode.Open, FileAccess.Read, FileShare.Read)); } - private void SaveSchema_Execute(object parameter) + [RelayCommand] + private void SaveSchema() { PssgSchema.SaveSchema(File.Open(schemaPath, FileMode.Create, FileAccess.Write, FileShare.Read)); } - private void ClearSchema_Execute(object parameter) + [RelayCommand] + private void ClearSchema() { PssgSchema.ClearSchema(); } @@ -248,57 +217,51 @@ public void LoadPssg(PssgFile pssg) nodesWorkspace.LoadData(file); texturesWorkspace.LoadData(nodesWorkspace.RootNode); - if (texturesWorkspace.Textures.Count > 0) SelectedTabIndex = 1; + SelectedTabIndex = texturesWorkspace.Textures.Count > 0 ? 1 : 0; _modelsWorkspace.LoadData(file); } - private void SavePssg(int type) + private async Task SavePssg(PssgFileType type) { - SaveFileDialog saveFileDialog = new SaveFileDialog(); - saveFileDialog.Filter = "PSSG files|*.pssg|DDS files|*.dds|Xml files|*.xml|All files|*.*"; - if (type == 3) - { - saveFileDialog.FilterIndex = 3; - } - else + FileSaveOptions saveOptions = new() { - saveFileDialog.FilterIndex = 1; - } - saveFileDialog.FileName = Path.GetFileNameWithoutExtension(filePath); - saveFileDialog.InitialDirectory = Path.GetDirectoryName(filePath); + Title = "Save pssg", + FileTypeChoices = [FilePickerTypes.Pssg, FilePickerFileTypes.Xml, FilePickerFileTypes.All], + SuggestedFileType = type switch + { + PssgFileType.Pssg => FilePickerTypes.Pssg, + PssgFileType.Xml => FilePickerFileTypes.Xml, + PssgFileType.CompressedPssg => FilePickerTypes.Pssg, + PssgFileType.CompressedXml => FilePickerFileTypes.Xml, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }, + }; + saveOptions.FileName = Path.GetFileNameWithoutExtension(filePath); + saveOptions.InitialDirectory = Path.GetDirectoryName(filePath); - if (saveFileDialog.ShowDialog() == true) + var result = await FileSaveInteraction.HandleAsync(saveOptions); + if (result is not null) { SaveTag(); try { - using (var fileStream = File.Open(saveFileDialog.FileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) + using (var fileStream = File.Open(result, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) { - if (type == 0) + if (type == file.FileType) { file.Save(fileStream); // Auto } - else if (type == 1) - { - file.FileType = PssgFileType.Pssg; - file.Save(fileStream); // Pssg - } - else if (type == 2) - { - file.FileType = PssgFileType.CompressedPssg; - file.Save(fileStream); - } else { - file.FileType = PssgFileType.Xml; + file.FileType = type; file.Save(fileStream); } } - filePath = saveFileDialog.FileName; + filePath = result; DisplayName = Properties.Resources.AppTitleShort + " - " + Path.GetFileName(filePath); } catch (Exception ex) { - MessageBox.Show("The program could not save this file! The error is displayed below:" + Environment.NewLine + Environment.NewLine + ex.Message, "Could Not Save", MessageBoxButton.OK, MessageBoxImage.Error); + await MessageBox.Show("The program could not save this file! The error is displayed below:" + Environment.NewLine + Environment.NewLine + ex.Message, "Could Not Save", MessageBoxButton.OK, MessageBoxImage.Error); } } } diff --git a/src/EgoPssgEditor/Models3d/ModelsWorkspaceViewModel.cs b/src/EgoPssgEditor/ViewModels/ModelsWorkspaceViewModel.cs similarity index 50% rename from src/EgoPssgEditor/Models3d/ModelsWorkspaceViewModel.cs rename to src/EgoPssgEditor/ViewModels/ModelsWorkspaceViewModel.cs index b7fd8cd..d4b4ba2 100644 --- a/src/EgoPssgEditor/Models3d/ModelsWorkspaceViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/ModelsWorkspaceViewModel.cs @@ -1,6 +1,6 @@ using EgoEngineLibrary.Formats.Pssg; using EgoEngineLibrary.Graphics; -using EgoPssgEditor.ViewModel; +using EgoPssgEditor.ViewModels; using Microsoft.Win32; using SharpGLTF.Schema2; using System; @@ -9,9 +9,17 @@ using System.Linq; using System.Windows; -namespace EgoPssgEditor.Models3d +using Avalonia.Controls; +using Avalonia.Platform.Storage; + +using CommunityToolkit.Mvvm.Input; + +using EgoEngineLibrary.Avalonia; +using EgoEngineLibrary.Avalonia.MessageBox; + +namespace EgoPssgEditor.ViewModels { - public class ModelsWorkspaceViewModel : WorkspaceViewModel + public partial class ModelsWorkspaceViewModel : WorkspaceViewModel { private PssgFile _pssg; PssgNodeViewModel rootNode; @@ -43,16 +51,6 @@ public ModelsWorkspaceViewModel(MainViewModel mainView) : base(mainView) { pssgNodes = new ObservableCollection(); - - Export = new RelayCommand(Export_Execute, Export_CanExecute); - Import = new RelayCommand(Import_Execute, Import_CanExecute); - - ExportDirt = new RelayCommand(ExportDirt_Execute, ExportDirt_CanExecute); - ImportDirt = new RelayCommand(ImportDirt_Execute, ImportDirt_CanExecute); - ImportGrid = new RelayCommand(ImportGrid_Execute, ImportGrid_CanExecute); - - ExportCarInterior = new RelayCommand(ExportCarInterior_Execute, ExportCarInterior_CanExecute); - ImportCarInterior = new RelayCommand(ImportCarInterior_Execute, ImportCarInterior_CanExecute); } public override void LoadData(object data) @@ -68,17 +66,8 @@ public override void ClearData() } #region Menu - public RelayCommand Export { get; } - public RelayCommand Import { get; } - - public RelayCommand ExportDirt { get; } - public RelayCommand ImportDirt { get; } - public RelayCommand ImportGrid { get; } - - public RelayCommand ExportCarInterior { get; } - public RelayCommand ImportCarInterior { get; } - private bool Export_CanExecute(object parameter) + private bool Export_CanExecute() { try { @@ -86,35 +75,39 @@ private bool Export_CanExecute(object parameter) } catch { return false; } } - private void Export_Execute(object parameter) + [RelayCommand(CanExecute = nameof(Export_CanExecute))] + private async Task ExportCar() { - SaveFileDialog dialog = new SaveFileDialog(); - dialog.Filter = "Gltf files|*.glb;*.gltf|All files|*.*"; - dialog.Title = "Select the model's save location and file name"; - dialog.DefaultExt = "glb"; + FileSaveOptions saveOptions = new() + { + FileTypeChoices = [FilePickerTypes.Gltf, FilePickerFileTypes.All], + Title = "Select the model's save location and file name", + DefaultExtension = "glb", + }; if (!string.IsNullOrEmpty(mainView.FilePath)) { - dialog.FileName = Path.GetFileNameWithoutExtension(mainView.FilePath); - dialog.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); + saveOptions.FileName = Path.GetFileNameWithoutExtension(mainView.FilePath); + saveOptions.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); } - if (dialog.ShowDialog() == true) + var result = await mainView.FileSaveInteraction.HandleAsync(saveOptions); + if (result is not null) { try { var converter = new CarExteriorPssgGltfConverter(); var model = converter.Convert(_pssg); - model.Save(dialog.FileName); + model.Save(result); } catch (Exception ex) { - MessageBox.Show("Could not export the model!" + Environment.NewLine + Environment.NewLine + + await MessageBox.Show("Could not export the model!" + Environment.NewLine + Environment.NewLine + ex.Message, Properties.Resources.AppTitleLong, MessageBoxButton.OK, MessageBoxImage.Error); } } } - private bool Import_CanExecute(object parameter) + private bool Import_CanExecute() { try { @@ -122,22 +115,26 @@ private bool Import_CanExecute(object parameter) } catch { return false; } } - private void Import_Execute(object parameter) + [RelayCommand(CanExecute = nameof(Import_CanExecute))] + private async Task ImportCar() { - OpenFileDialog dialog = new OpenFileDialog(); - dialog.Filter = "Gltf files|*.glb;*.gltf|All files|*.*"; - dialog.Title = "Select a gltf model file"; + FileOpenOptions openOptions = new() + { + FileTypeChoices = [FilePickerTypes.Gltf, FilePickerFileTypes.All], + Title = "Select a gltf model file", + }; if (!string.IsNullOrEmpty(mainView.FilePath)) { - dialog.FileName = Path.GetFileNameWithoutExtension(mainView.FilePath); - dialog.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); + openOptions.FileName = Path.GetFileNameWithoutExtension(mainView.FilePath); + openOptions.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); } - if (dialog.ShowDialog() == true) + var result = await mainView.FileOpenInteraction.HandleAsync(openOptions); + if (result is not null) { try { - var gltf = ModelRoot.Load(dialog.FileName); + var gltf = ModelRoot.Load(result); var conv = new GltfCarExteriorPssgConverter(); conv.Convert(gltf, _pssg); @@ -146,13 +143,13 @@ private void Import_Execute(object parameter) } catch (Exception ex) { - MessageBox.Show("Could not import the model!" + Environment.NewLine + Environment.NewLine + + await MessageBox.Show("Could not import the model!" + Environment.NewLine + Environment.NewLine + ex.Message, Properties.Resources.AppTitleLong, MessageBoxButton.OK, MessageBoxImage.Error); } } } - private bool ExportDirt_CanExecute(object parameter) + private bool ExportDirt_CanExecute() { try { @@ -160,35 +157,39 @@ private bool ExportDirt_CanExecute(object parameter) } catch { return false; } } - private void ExportDirt_Execute(object parameter) + [RelayCommand(CanExecute = nameof(ExportDirt_CanExecute))] + private async Task ExportDirt() { - SaveFileDialog dialog = new SaveFileDialog(); - dialog.Filter = "Gltf files|*.glb;*.gltf|All files|*.*"; - dialog.Title = "Select the model's save location and file name"; - dialog.DefaultExt = "glb"; + FileSaveOptions saveOptions = new() + { + FileTypeChoices = [FilePickerTypes.Gltf, FilePickerFileTypes.All], + Title = "Select the model's save location and file name", + DefaultExtension = "glb", + }; if (!string.IsNullOrEmpty(mainView.FilePath)) { - dialog.FileName = Path.GetFileNameWithoutExtension(mainView.FilePath); - dialog.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); + saveOptions.FileName = Path.GetFileNameWithoutExtension(mainView.FilePath); + saveOptions.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); } - if (dialog.ShowDialog() == true) + var result = await mainView.FileSaveInteraction.HandleAsync(saveOptions); + if (result is not null) { try { var converter = new DirtCarExteriorPssgGltfConverter(); var model = converter.Convert(_pssg); - model.Save(dialog.FileName); + model.Save(result); } catch (Exception ex) { - MessageBox.Show("Could not export the model!" + Environment.NewLine + Environment.NewLine + + await MessageBox.Show("Could not export the model!" + Environment.NewLine + Environment.NewLine + ex.Message, Properties.Resources.AppTitleLong, MessageBoxButton.OK, MessageBoxImage.Error); } } } - private bool ImportDirt_CanExecute(object parameter) + private bool ImportDirt_CanExecute() { try { @@ -196,22 +197,26 @@ private bool ImportDirt_CanExecute(object parameter) } catch { return false; } } - private void ImportDirt_Execute(object parameter) + [RelayCommand(CanExecute = nameof(ImportDirt_CanExecute))] + private async Task ImportDirt() { - OpenFileDialog dialog = new OpenFileDialog(); - dialog.Filter = "Gltf files|*.glb;*.gltf|All files|*.*"; - dialog.Title = "Select a gltf model file"; + FileOpenOptions openOptions = new() + { + FileTypeChoices = [FilePickerTypes.Gltf, FilePickerFileTypes.All], + Title = "Select a gltf model file", + }; if (!string.IsNullOrEmpty(mainView.FilePath)) { - dialog.FileName = Path.GetFileNameWithoutExtension(mainView.FilePath); - dialog.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); + openOptions.FileName = Path.GetFileNameWithoutExtension(mainView.FilePath); + openOptions.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); } - if (dialog.ShowDialog() == true) + var result = await mainView.FileOpenInteraction.HandleAsync(openOptions); + if (result is not null) { try { - var gltf = ModelRoot.Load(dialog.FileName); + var gltf = ModelRoot.Load(result); var conv = new GltfDirtCarExteriorPssgConverter(); conv.Convert(gltf, _pssg); @@ -220,13 +225,13 @@ private void ImportDirt_Execute(object parameter) } catch (Exception ex) { - MessageBox.Show("Could not import the model!" + Environment.NewLine + Environment.NewLine + + await MessageBox.Show("Could not import the model!" + Environment.NewLine + Environment.NewLine + ex.Message, Properties.Resources.AppTitleLong, MessageBoxButton.OK, MessageBoxImage.Error); } } } - private bool ImportGrid_CanExecute(object parameter) + private bool ImportGrid_CanExecute() { try { @@ -234,22 +239,26 @@ private bool ImportGrid_CanExecute(object parameter) } catch { return false; } } - private void ImportGrid_Execute(object parameter) + [RelayCommand(CanExecute = nameof(ImportGrid_CanExecute))] + private async Task ImportGrid() { - OpenFileDialog dialog = new OpenFileDialog(); - dialog.Filter = "Gltf files|*.glb;*.gltf|All files|*.*"; - dialog.Title = "Select a gltf model file"; + FileOpenOptions openOptions = new() + { + FileTypeChoices = [FilePickerTypes.Gltf, FilePickerFileTypes.All], + Title = "Select a gltf model file", + }; if (!string.IsNullOrEmpty(mainView.FilePath)) { - dialog.FileName = Path.GetFileNameWithoutExtension(mainView.FilePath); - dialog.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); + openOptions.FileName = Path.GetFileNameWithoutExtension(mainView.FilePath); + openOptions.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); } - if (dialog.ShowDialog() == true) + var result = await mainView.FileOpenInteraction.HandleAsync(openOptions); + if (result is not null) { try { - var gltf = ModelRoot.Load(dialog.FileName); + var gltf = ModelRoot.Load(result); var conv = new GltfGridCarExteriorPssgConverter(); conv.Convert(gltf, _pssg); @@ -258,13 +267,13 @@ private void ImportGrid_Execute(object parameter) } catch (Exception ex) { - MessageBox.Show("Could not import the model!" + Environment.NewLine + Environment.NewLine + + await MessageBox.Show("Could not import the model!" + Environment.NewLine + Environment.NewLine + ex.Message, Properties.Resources.AppTitleLong, MessageBoxButton.OK, MessageBoxImage.Error); } } } - private bool ExportCarInterior_CanExecute(object parameter) + private bool ExportCarInterior_CanExecute() { try { @@ -272,36 +281,40 @@ private bool ExportCarInterior_CanExecute(object parameter) } catch { return false; } } - private void ExportCarInterior_Execute(object parameter) + [RelayCommand(CanExecute = nameof(ExportCarInterior_CanExecute))] + private async Task ExportCarInterior() { - SaveFileDialog dialog = new SaveFileDialog(); - dialog.Filter = "Gltf files|*.glb;*.gltf|All files|*.*"; - dialog.Title = "Select the model's save location and file name"; - dialog.DefaultExt = "glb"; + FileSaveOptions saveOptions = new() + { + FileTypeChoices = [FilePickerTypes.Gltf, FilePickerFileTypes.All], + Title = "Select the model's save location and file name", + DefaultExtension = "glb", + }; if (!string.IsNullOrEmpty(mainView.FilePath)) { - dialog.FileName = Path.GetFileNameWithoutExtension(mainView.FilePath); - dialog.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); + saveOptions.FileName = Path.GetFileNameWithoutExtension(mainView.FilePath); + saveOptions.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); } - if (dialog.ShowDialog() == true) + var result = await mainView.FileSaveInteraction.HandleAsync(saveOptions); + if (result is not null) { try { var converter = new CarInteriorPssgGltfConverter(); var model = converter.Convert(_pssg); - model.Save(dialog.FileName); + model.Save(result); } catch (Exception ex) { - MessageBox.Show("Could not export the model!" + Environment.NewLine + Environment.NewLine + + await MessageBox.Show("Could not export the model!" + Environment.NewLine + Environment.NewLine + ex.Message, Properties.Resources.AppTitleLong, MessageBoxButton.OK, MessageBoxImage.Error); } } } - private bool ImportCarInterior_CanExecute(object parameter) + private bool ImportCarInterior_CanExecute() { try { @@ -309,22 +322,26 @@ private bool ImportCarInterior_CanExecute(object parameter) } catch { return false; } } - private void ImportCarInterior_Execute(object parameter) + [RelayCommand(CanExecute = nameof(ImportCarInterior_CanExecute))] + private async Task ImportCarInterior() { - OpenFileDialog dialog = new OpenFileDialog(); - dialog.Filter = "Gltf files|*.glb;*.gltf|All files|*.*"; - dialog.Title = "Select a gltf model file"; + FileOpenOptions openOptions = new() + { + FileTypeChoices = [FilePickerTypes.Gltf, FilePickerFileTypes.All], + Title = "Select a gltf model file", + }; if (!string.IsNullOrEmpty(mainView.FilePath)) { - dialog.FileName = Path.GetFileNameWithoutExtension(mainView.FilePath); - dialog.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); + openOptions.FileName = Path.GetFileNameWithoutExtension(mainView.FilePath); + openOptions.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); } - if (dialog.ShowDialog() == true) + var result = await mainView.FileOpenInteraction.HandleAsync(openOptions); + if (result is not null) { try { - var gltf = ModelRoot.Load(dialog.FileName); + var gltf = ModelRoot.Load(result); var conv = new GltfCarInteriorPssgConverter(); conv.Convert(gltf, _pssg); @@ -333,7 +350,7 @@ private void ImportCarInterior_Execute(object parameter) } catch (Exception ex) { - MessageBox.Show("Could not import the model!" + Environment.NewLine + Environment.NewLine + + await MessageBox.Show("Could not import the model!" + Environment.NewLine + Environment.NewLine + ex.Message, Properties.Resources.AppTitleLong, MessageBoxButton.OK, MessageBoxImage.Error); } } diff --git a/src/EgoPssgEditor/ViewModel/NodesWorkspaceViewModel.cs b/src/EgoPssgEditor/ViewModels/NodesWorkspaceViewModel.cs similarity index 65% rename from src/EgoPssgEditor/ViewModel/NodesWorkspaceViewModel.cs rename to src/EgoPssgEditor/ViewModels/NodesWorkspaceViewModel.cs index b531081..1b6a8a9 100644 --- a/src/EgoPssgEditor/ViewModel/NodesWorkspaceViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/NodesWorkspaceViewModel.cs @@ -12,11 +12,19 @@ using System.Xml; using System.Xml.Linq; +using Avalonia.Platform.Storage; + +using CommunityToolkit.Mvvm.Input; + +using EgoEngineLibrary.Avalonia; +using EgoEngineLibrary.Avalonia.MessageBox; using EgoEngineLibrary.Conversion; -namespace EgoPssgEditor.ViewModel +using EgoPssgEditor.Views; + +namespace EgoPssgEditor.ViewModels { - public class NodesWorkspaceViewModel : WorkspaceViewModel + public partial class NodesWorkspaceViewModel : WorkspaceViewModel { #region Data PssgNodeViewModel rootNode; @@ -52,18 +60,6 @@ public NodesWorkspaceViewModel(MainViewModel mainView) : base(mainView) { pssgNodes = new ObservableCollection(); - - export = new RelayCommand(Export_Execute, Export_CanExecute); - import = new RelayCommand(Import_Execute, Import_CanExecute); - exportData = new RelayCommand(ExportData_Execute, ExportData_CanExecute); - importData = new RelayCommand(ImportData_Execute, ImportData_CanExecute); - - addNode = new RelayCommand(AddNode_Execute, AddNode_CanExecute); - removeNode = new RelayCommand(RemoveNode_Execute, RemoveNode_CanExecute); - CloneNode = new RelayCommand(CloneNode_Execute, CloneNode_CanExecute); - - addAttribute = new RelayCommand(AddAttribute_Execute, AddAttribute_CanExecute); - removeAttribute = new RelayCommand(RemoveAttribute_Execute, RemoveAttribute_CanExecute); } public override void LoadData(object data) @@ -79,65 +75,25 @@ public override void ClearData() } #region Menu - readonly RelayCommand export; - readonly RelayCommand import; - readonly RelayCommand exportData; - readonly RelayCommand importData; - - readonly RelayCommand addNode; - readonly RelayCommand removeNode; - readonly RelayCommand addAttribute; - readonly RelayCommand removeAttribute; - - public RelayCommand Export - { - get { return export; } - } - public RelayCommand Import - { - get { return import; } - } - public RelayCommand ExportData - { - get { return exportData; } - } - public RelayCommand ImportData - { - get { return importData; } - } - - public RelayCommand AddNode - { - get { return addNode; } - } - public RelayCommand RemoveNode - { - get { return removeNode; } - } - public RelayCommand CloneNode { get; } - - public RelayCommand AddAttribute - { - get { return addAttribute; } - } - public RelayCommand RemoveAttribute - { - get { return removeAttribute; } - } private bool Export_CanExecute(object parameter) { return parameter != null; } - private void Export_Execute(object parameter) + [RelayCommand(CanExecute = nameof(Export_CanExecute))] + private async Task Export(object parameter) { PssgNode node = ((PssgNodeViewModel)parameter).Node; - SaveFileDialog dialog = new SaveFileDialog(); - dialog.Filter = "Xml files|*.xml|All files|*.*"; - dialog.Title = "Select the node's save location and file name"; - dialog.DefaultExt = "xml"; - dialog.FileName = "node.xml"; - if (dialog.ShowDialog() == true) + FileSaveOptions saveOptions = new() + { + FileTypeChoices = [FilePickerFileTypes.Xml, FilePickerFileTypes.All], + Title = "Select the node's save location and file name", + DefaultExtension = "xml", + FileName = "node.xml", + }; + + var result = await mainView.FileSaveInteraction.HandleAsync(saveOptions); + if (result is not null) { try { @@ -153,14 +109,14 @@ private void Export_Execute(object parameter) XElement pssg = (XElement)xDoc.FirstNode; node.WriteXml(pssg); - using (XmlWriter writer = XmlWriter.Create(File.Open(dialog.FileName, FileMode.Create, FileAccess.Write, FileShare.Read), settings)) + using (XmlWriter writer = XmlWriter.Create(File.Open(result, FileMode.Create, FileAccess.Write, FileShare.Read), settings)) { xDoc.Save(writer); } } catch (Exception ex) { - MessageBox.Show("Could not export the node!" + Environment.NewLine + Environment.NewLine + + await MessageBox.Show("Could not export the node!" + Environment.NewLine + Environment.NewLine + ex.Message, "Export Failed", MessageBoxButton.OK, MessageBoxImage.Error); } } @@ -169,19 +125,24 @@ private bool Import_CanExecute(object parameter) { return parameter != null; } - private void Import_Execute(object parameter) + [RelayCommand(CanExecute = nameof(Import_CanExecute))] + private async Task Import(object parameter) { PssgNodeViewModel nodeView = (PssgNodeViewModel)parameter; - OpenFileDialog dialog = new OpenFileDialog(); - dialog.Filter = "Xml files|*.xml|All files|*.*"; - dialog.Title = "Select a xml file"; - dialog.FileName = "node.xml"; - if (dialog.ShowDialog() == true) + FileOpenOptions openOptions = new() + { + FileTypeChoices = [FilePickerFileTypes.Xml, FilePickerFileTypes.All], + Title = "Select a xml file", + FileName = "node.xml", + }; + + var result = await mainView.FileOpenInteraction.HandleAsync(openOptions); + if (result is not null) { try { PssgNode node = nodeView.Node; - using (FileStream fileStream = File.Open(dialog.FileName, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (FileStream fileStream = File.Open(result, FileMode.Open, FileAccess.Read, FileShare.Read)) { XDocument xDoc = XDocument.Load(fileStream); @@ -207,7 +168,7 @@ private void Import_Execute(object parameter) } catch (Exception ex) { - MessageBox.Show("Could not import the node!" + Environment.NewLine + Environment.NewLine + + await MessageBox.Show("Could not import the node!" + Environment.NewLine + Environment.NewLine + ex.Message, "Import Failed", MessageBoxButton.OK, MessageBoxImage.Error); } } @@ -219,20 +180,25 @@ private bool ExportData_CanExecute(object parameter) return ((PssgNodeViewModel)parameter).IsDataNode; } - private void ExportData_Execute(object parameter) + [RelayCommand(CanExecute = nameof(ExportData_CanExecute))] + private async Task ExportData(object parameter) { PssgNodeViewModel nodeView = (PssgNodeViewModel)parameter; - SaveFileDialog dialog = new SaveFileDialog(); - dialog.Filter = "Bin files|*.bin|All files|*.*"; - dialog.Title = "Select the byte data save location and file name"; - dialog.DefaultExt = "bin"; - dialog.FileName = "nodeData.bin"; - if (dialog.ShowDialog() == true) + FileSaveOptions saveOptions = new() + { + FileTypeChoices = [FilePickerTypes.Bin, FilePickerFileTypes.All], + Title = "Select the byte data save location and file name", + DefaultExtension = "bin", + FileName = "nodeData.bin", + }; + + var result = await mainView.FileSaveInteraction.HandleAsync(saveOptions); + if (result is not null) { try { PssgNode node = nodeView.Node; - using (var fs = File.Open(dialog.FileName, FileMode.Create)) + using (var fs = File.Open(result, FileMode.Create)) using (PssgBinaryWriter writer = new PssgBinaryWriter(EndianBitConverter.Big, fs, false)) { writer.WriteObject(node.Value); @@ -240,7 +206,7 @@ private void ExportData_Execute(object parameter) } catch (Exception ex) { - MessageBox.Show("Could not export data!" + Environment.NewLine + Environment.NewLine + + await MessageBox.Show("Could not export data!" + Environment.NewLine + Environment.NewLine + ex.Message, "Export Failed", MessageBoxButton.OK, MessageBoxImage.Error); } } @@ -251,19 +217,24 @@ private bool ImportData_CanExecute(object parameter) return ((PssgNodeViewModel)parameter).IsDataNode; } - private void ImportData_Execute(object parameter) + [RelayCommand(CanExecute = nameof(ImportData_CanExecute))] + private async Task ImportData(object parameter) { PssgNodeViewModel nodeView = (PssgNodeViewModel)parameter; - OpenFileDialog dialog = new OpenFileDialog(); - dialog.Filter = "Bin files|*.bin|All files|*.*"; - dialog.Title = "Select a bin file"; - dialog.FileName = "nodeData.bin"; - if (dialog.ShowDialog() == true) + FileOpenOptions openOptions = new() + { + FileTypeChoices = [FilePickerTypes.Bin, FilePickerFileTypes.All], + Title = "Select a bin file", + FileName = "nodeData.bin", + }; + + var result = await mainView.FileOpenInteraction.HandleAsync(openOptions); + if (result is not null) { try { PssgNode node = nodeView.Node; - using (var fs = File.Open(dialog.FileName, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var fs = File.Open(result, FileMode.Open, FileAccess.Read, FileShare.Read)) using (PssgBinaryReader reader = new PssgBinaryReader(EndianBitConverter.Big, fs, false)) { node.Value = reader.ReadNodeValue((int)reader.BaseStream.Length); @@ -273,7 +244,7 @@ private void ImportData_Execute(object parameter) } catch (Exception ex) { - MessageBox.Show("Could not import data!" + Environment.NewLine + Environment.NewLine + + await MessageBox.Show("Could not import data!" + Environment.NewLine + Environment.NewLine + ex.Message, "Import Failed", MessageBoxButton.OK, MessageBoxImage.Error); } } @@ -283,17 +254,18 @@ private bool AddNode_CanExecute(object parameter) { return parameter != null; } - private void AddNode_Execute(object parameter) + [RelayCommand(CanExecute = nameof(AddNode_CanExecute))] + private async Task AddNode(object parameter) { PssgNodeViewModel nodeView = (PssgNodeViewModel)parameter; if (nodeView.Node.IsDataNode) { - MessageBox.Show("Adding sub nodes to a node with data is not allowed!", Properties.Resources.AppTitleShort, MessageBoxButton.OK, MessageBoxImage.Stop); + await MessageBox.Show("Adding sub nodes to a node with data is not allowed!", Properties.Resources.AppTitleShort, MessageBoxButton.OK, MessageBoxImage.Stop); return; } AddNodeWindow aaw = new AddNodeWindow(); - if (aaw.ShowDialog() == true) + if (await aaw.ShowDialog() == true) { PssgNode newNode = nodeView.Node.AppendChild(aaw.NodeName); @@ -310,7 +282,8 @@ private bool RemoveNode_CanExecute(object parameter) { return parameter != null && parameter != RootNode; } - private void RemoveNode_Execute(object parameter) + [RelayCommand(CanExecute = nameof(RemoveNode_CanExecute))] + private void RemoveNode(object parameter) { PssgNodeViewModel nodeView = (PssgNodeViewModel)parameter; nodeView.Node.ParentNode.RemoveChild(nodeView.Node); @@ -322,7 +295,8 @@ private bool CloneNode_CanExecute(object parameter) { return parameter != null && parameter != RootNode; } - private void CloneNode_Execute(object parameter) + [RelayCommand(CanExecute = nameof(CloneNode_CanExecute))] + private async Task CloneNode(object parameter) { try { @@ -341,7 +315,7 @@ private void CloneNode_Execute(object parameter) } catch (Exception ex) { - MessageBox.Show("Could not clone node!" + Environment.NewLine + Environment.NewLine + + await MessageBox.Show("Could not clone node!" + Environment.NewLine + Environment.NewLine + ex.Message, Properties.Resources.AppTitleLong, MessageBoxButton.OK, MessageBoxImage.Error); } } @@ -350,12 +324,13 @@ private bool AddAttribute_CanExecute(object parameter) { return parameter != null; } - private void AddAttribute_Execute(object parameter) + [RelayCommand(CanExecute = nameof(AddAttribute_CanExecute))] + private async Task AddAttribute(object parameter) { PssgNodeViewModel nodeView = (PssgNodeViewModel)parameter; AddAttributeWindow aaw = new AddAttributeWindow(); - if (aaw.ShowDialog() == true) + if (await aaw.ShowDialog() == true) { PssgAttribute attr = nodeView.Node.AddAttribute(aaw.AttributeName, Convert.ChangeType(aaw.Value, aaw.AttributeValueType)); if (attr == null) @@ -371,7 +346,8 @@ private bool RemoveAttribute_CanExecute(object parameter) { return parameter != null; } - private void RemoveAttribute_Execute(object parameter) + [RelayCommand(CanExecute = nameof(RemoveAttribute_CanExecute))] + private void RemoveAttribute(object parameter) { PssgAttributeViewModel attrView = (PssgAttributeViewModel)parameter; diff --git a/src/EgoPssgEditor/ViewModel/PssgAttributeViewModel.cs b/src/EgoPssgEditor/ViewModels/PssgAttributeViewModel.cs similarity index 96% rename from src/EgoPssgEditor/ViewModel/PssgAttributeViewModel.cs rename to src/EgoPssgEditor/ViewModels/PssgAttributeViewModel.cs index bc0c1b3..e5fbd6d 100644 --- a/src/EgoPssgEditor/ViewModel/PssgAttributeViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/PssgAttributeViewModel.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace EgoPssgEditor.ViewModel +namespace EgoPssgEditor.ViewModels { public class PssgAttributeViewModel : ViewModelBase { diff --git a/src/EgoPssgEditor/ViewModel/PssgNodeViewModel.cs b/src/EgoPssgEditor/ViewModels/PssgNodeViewModel.cs similarity index 99% rename from src/EgoPssgEditor/ViewModel/PssgNodeViewModel.cs rename to src/EgoPssgEditor/ViewModels/PssgNodeViewModel.cs index bd4102a..dbba083 100644 --- a/src/EgoPssgEditor/ViewModel/PssgNodeViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/PssgNodeViewModel.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EgoPssgEditor.ViewModel +namespace EgoPssgEditor.ViewModels { public class PssgNodeViewModel : ViewModelBase { diff --git a/src/EgoPssgEditor/ViewModel/PssgTextureViewModel.cs b/src/EgoPssgEditor/ViewModels/PssgTextureViewModel.cs similarity index 74% rename from src/EgoPssgEditor/ViewModel/PssgTextureViewModel.cs rename to src/EgoPssgEditor/ViewModels/PssgTextureViewModel.cs index 1d67358..9166018 100644 --- a/src/EgoPssgEditor/ViewModel/PssgTextureViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/PssgTextureViewModel.cs @@ -6,10 +6,12 @@ using System.IO; using System.Runtime.InteropServices; using System.Windows; -using System.Windows.Media; -using System.Windows.Media.Imaging; -namespace EgoPssgEditor.ViewModel +using Avalonia; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace EgoPssgEditor.ViewModels { public class PssgTextureViewModel : ViewModelBase { @@ -41,9 +43,9 @@ public int Height #region Presentation Props bool isSelected; - BitmapSource preview; + Bitmap preview; string previewError; - Visibility previewErrorVisibility; + bool previewErrorVisibility; public bool IsSelected { @@ -66,7 +68,7 @@ public bool IsSelected } } } - public BitmapSource Preview + public Bitmap Preview { get { return preview; } set { preview = value; OnPropertyChanged(nameof(Preview)); } @@ -76,7 +78,7 @@ public string PreviewError get { return previewError; } set { previewError = value; OnPropertyChanged(nameof(PreviewError)); } } - public Visibility PreviewErrorVisibility + public bool PreviewErrorVisibility { get { return previewErrorVisibility; } set { previewErrorVisibility = value; OnPropertyChanged(nameof(PreviewErrorVisibility)); } @@ -110,22 +112,36 @@ public void GetPreview() var decoder = new BcDecoder(); if (decoder.IsHdrFormat(bcDds)) { - var pixels = new byte[width * height * 3]; - var pixelsSpan = MemoryMarshal.Cast(pixels.AsSpan()); - decoder.DecodeDdsToPixels(bcDds, pixelsSpan); - var bmSource = BitmapSource.Create(width, height, 96.0, 96.0, PixelFormats.Bgr24, null, pixels, width * 3); + var bmSource = new WriteableBitmap(new PixelSize(width, height), new Vector(96, 96), + PixelFormats.Bgr24); + using (var frameBuffer = bmSource.Lock()) + { + unsafe + { + var pixelsSpan = new Span(frameBuffer.Address.ToPointer(), width * height * 3); + decoder.DecodeDdsToPixels(bcDds, pixelsSpan); + } + } + Preview = bmSource; } else { - var pixels = new byte[width * height * 4]; - var pixelsSpan = MemoryMarshal.Cast(pixels.AsSpan()); - decoder.DecodeDdsToPixels(bcDds, pixelsSpan); - var bmSource = BitmapSource.Create(width, height, 96.0, 96.0, PixelFormats.Bgra32, null, pixels, width * 4); + var bmSource = new WriteableBitmap(new PixelSize(width, height), new Vector(96, 96), + PixelFormats.Bgra8888, AlphaFormat.Unpremul); + using (var frameBuffer = bmSource.Lock()) + { + unsafe + { + var pixelsSpan = new Span(frameBuffer.Address.ToPointer(), width * height * 4); + decoder.DecodeDdsToPixels(bcDds, pixelsSpan); + } + } + Preview = bmSource; } - PreviewErrorVisibility = Visibility.Collapsed; + PreviewErrorVisibility = false; OnPropertyChanged(nameof(Width)); OnPropertyChanged(nameof(Height)); OnPropertyChanged(nameof(TextureInfo)); @@ -137,7 +153,7 @@ public void GetPreview() PreviewError = "Could not create preview! Export/Import may still work." + Environment.NewLine + Environment.NewLine + ex.Message; else PreviewError = "Could not create preview! Failed to convert pssg texture to dds." + Environment.NewLine + Environment.NewLine + ex.Message; - PreviewErrorVisibility = Visibility.Visible; + PreviewErrorVisibility = true; } } } diff --git a/src/EgoPssgEditor/ViewModel/TexturesWorkspaceViewModel.cs b/src/EgoPssgEditor/ViewModels/TexturesWorkspaceViewModel.cs similarity index 66% rename from src/EgoPssgEditor/ViewModel/TexturesWorkspaceViewModel.cs rename to src/EgoPssgEditor/ViewModels/TexturesWorkspaceViewModel.cs index 920b733..0a96f7d 100644 --- a/src/EgoPssgEditor/ViewModel/TexturesWorkspaceViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/TexturesWorkspaceViewModel.cs @@ -4,16 +4,27 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; -using System.Windows.Data; -namespace EgoPssgEditor.ViewModel +using ActiproSoftware.UI.Avalonia.Data; + +using Avalonia.Platform.Storage; + +using CommunityToolkit.Mvvm.Input; + +using EgoEngineLibrary.Avalonia; +using EgoEngineLibrary.Avalonia.MessageBox; + +using EgoPssgEditor.Views; + +namespace EgoPssgEditor.ViewModels { - public class TexturesWorkspaceViewModel : WorkspaceViewModel + public partial class TexturesWorkspaceViewModel : WorkspaceViewModel { #region Data readonly ObservableCollection textures; @@ -30,8 +41,10 @@ public ObservableCollection Textures #endregion #region Presentation Data - readonly CollectionView texturesViewSource; + readonly CollectionView texturesViewSource; string filterText; + + public ICollectionView TexturesViewSource => texturesViewSource; public string FilterText { @@ -51,16 +64,9 @@ public TexturesWorkspaceViewModel(MainViewModel mainView) : base(mainView) { textures = new ObservableCollection(); - texturesViewSource = (CollectionView)CollectionViewSource.GetDefaultView(Textures); + texturesViewSource = new CollectionView(Textures); texturesViewSource.Filter += TextureFilter; - texturesViewSource.SortDescriptions.Add(new System.ComponentModel.SortDescription(nameof(DisplayName), System.ComponentModel.ListSortDirection.Ascending)); - - export = new RelayCommand(Export_Execute, Export_CanExecute); - import = new RelayCommand(Import_Execute, Import_CanExecute); - exportTextures = new RelayCommand(ExportTextures_Execute, ExportTextures_CanExecute); - importTextures = new RelayCommand(ImportTextures_Execute, ImportTextures_CanExecute); - duplicateTexture = new RelayCommand(DuplicateTexture_Execute, DuplicateTexture_CanExecute); - removeTextureC = new RelayCommand(RemoveTextureC_Execute, RemoveTextureC_CanExecute); + texturesViewSource.SortDescriptions.Add(new SortDescription(x => x.DisplayName)); } public override void LoadData(object data) @@ -121,60 +127,34 @@ private bool TextureFilter(object item) } #region Menu - readonly RelayCommand export; - readonly RelayCommand import; - readonly RelayCommand exportTextures; - readonly RelayCommand importTextures; - readonly RelayCommand duplicateTexture; - readonly RelayCommand removeTextureC; - - public RelayCommand Export - { - get { return export; } - } - public RelayCommand Import - { - get { return import; } - } - public RelayCommand ExportTextures - { - get { return exportTextures; } - } - public RelayCommand ImportTextures - { - get { return importTextures; } - } - public RelayCommand DuplicateTexture - { - get { return duplicateTexture; } - } - public RelayCommand RemoveTextureC - { - get { return removeTextureC; } - } private bool Export_CanExecute(object parameter) { return parameter != null; } - private void Export_Execute(object parameter) + [RelayCommand(CanExecute = nameof(Export_CanExecute))] + private async Task Export(object parameter) { PssgNode node = ((PssgTextureViewModel)parameter).Texture; - SaveFileDialog dialog = new SaveFileDialog(); - dialog.Filter = "Dds files|*.dds|All files|*.*"; - dialog.Title = "Select the dds save location and file name"; - dialog.FileName = node.Attributes["id"].DisplayValue + ".dds"; - if (dialog.ShowDialog() == true) + FileSaveOptions saveOptions = new() + { + FileTypeChoices = [FilePickerTypes.Dds, FilePickerFileTypes.All], + Title = "Select the dds save location and file name", + FileName = node.Attributes["id"].DisplayValue + ".dds", + }; + + var result = await mainView.FileSaveInteraction.HandleAsync(saveOptions); + if (result is not null) { try { DdsFile dds = node.ToDdsFile(false); - using (var fs = File.Open(dialog.FileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) + using (var fs = File.Open(result, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) dds.Write(fs, -1); } catch (Exception ex) { - MessageBox.Show("Could not export texture!" + Environment.NewLine + Environment.NewLine + + await MessageBox.Show("Could not export texture!" + Environment.NewLine + Environment.NewLine + ex.Message, "Export Failed", MessageBoxButton.OK, MessageBoxImage.Error); } } @@ -183,35 +163,41 @@ private bool Import_CanExecute(object parameter) { return parameter != null; } - private void Import_Execute(object parameter) + [RelayCommand(CanExecute = nameof(Import_CanExecute))] + private async Task Import(object parameter) { PssgTextureViewModel texView = (PssgTextureViewModel)parameter; PssgNode node = texView.Texture; - OpenFileDialog dialog = new OpenFileDialog(); - dialog.Filter = "Dds files|*.dds|All files|*.*"; - dialog.Title = "Select a dds file"; - dialog.FileName = node.Attributes["id"].DisplayValue + ".dds"; - if (dialog.ShowDialog() == true) + FileOpenOptions openOptions = new() + { + FileTypeChoices = [FilePickerTypes.Dds, FilePickerFileTypes.All], + Title = "Select a dds file", + FileName = node.Attributes["id"].DisplayValue + ".dds", + }; + + var result = await mainView.FileOpenInteraction.HandleAsync(openOptions); + if (result is not null) { try { - DdsFile dds = new DdsFile(File.Open(dialog.FileName, FileMode.Open)); + DdsFile dds = new DdsFile(File.Open(result, FileMode.Open)); dds.ToPssgNode(node); texView.GetPreview(); } catch (Exception ex) { - MessageBox.Show("Could not import texture!" + Environment.NewLine + Environment.NewLine + + await MessageBox.Show("Could not import texture!" + Environment.NewLine + Environment.NewLine + ex.Message, "Import Failed", MessageBoxButton.OK, MessageBoxImage.Error); } } } - private bool ExportTextures_CanExecute(object parameter) + private bool ExportTextures_CanExecute() { return Textures.Count > 0; } - private void ExportTextures_Execute(object parameter) + [RelayCommand(CanExecute = nameof(ExportTextures_CanExecute))] + private async Task ExportTextures() { try { @@ -225,18 +211,19 @@ private void ExportTextures_Execute(object parameter) using (var fs = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) dds.Write(fs, -1); } - MessageBox.Show("Textures exported successfully!", "Success", MessageBoxButton.OK, MessageBoxImage.Information); + await MessageBox.Show("Textures exported successfully!", "Success", MessageBoxButton.OK, MessageBoxImage.Information); } catch { - MessageBox.Show("There was an error, could not export all textures!", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + await MessageBox.Show("There was an error, could not export all textures!", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } - private bool ImportTextures_CanExecute(object parameter) + private bool ImportTextures_CanExecute() { return Textures.Count > 0; } - private void ImportTextures_Execute(object parameter) + [RelayCommand(CanExecute = nameof(ImportTextures_CanExecute))] + private async Task ImportTextures() { try { @@ -261,29 +248,30 @@ private void ImportTextures_Execute(object parameter) } } - MessageBox.Show("Textures imported successfully!", "Success", MessageBoxButton.OK, MessageBoxImage.Information); + await MessageBox.Show("Textures imported successfully!", "Success", MessageBoxButton.OK, MessageBoxImage.Information); } else { - MessageBox.Show("Could not find textures folder!", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + await MessageBox.Show("Could not find textures folder!", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } catch { - MessageBox.Show("There was an error, could not import all textures!", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + await MessageBox.Show("There was an error, could not import all textures!", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } private bool DuplicateTexture_CanExecute(object parameter) { return parameter != null; } - private void DuplicateTexture_Execute(object parameter) + [RelayCommand(CanExecute = nameof(DuplicateTexture_CanExecute))] + private async Task DuplicateTexture(object parameter) { PssgTextureViewModel texView = (PssgTextureViewModel)parameter; DuplicateTextureWindow dtw = new DuplicateTextureWindow(); dtw.TextureName = texView.DisplayName + "_2"; - if (dtw.ShowDialog() == true) + if (await dtw.ShowDialog() == true) { // Copy and Edit Name PssgNode nodeToCopy = texView.Texture; @@ -311,7 +299,8 @@ private bool RemoveTextureC_CanExecute(object parameter) { return parameter != null && ((PssgTextureViewModel)parameter).Texture != mainView.PssgFile.RootNode; } - private void RemoveTextureC_Execute(object parameter) + [RelayCommand(CanExecute = nameof(RemoveTextureC_CanExecute))] + private void RemoveTextureC(object parameter) { PssgTextureViewModel texView = (PssgTextureViewModel)parameter; diff --git a/src/EgoPssgEditor/ViewModel/ViewModelBase.cs b/src/EgoPssgEditor/ViewModels/ViewModelBase.cs similarity index 100% rename from src/EgoPssgEditor/ViewModel/ViewModelBase.cs rename to src/EgoPssgEditor/ViewModels/ViewModelBase.cs diff --git a/src/EgoPssgEditor/ViewModel/WorkspaceViewModel.cs b/src/EgoPssgEditor/ViewModels/WorkspaceViewModel.cs similarity index 93% rename from src/EgoPssgEditor/ViewModel/WorkspaceViewModel.cs rename to src/EgoPssgEditor/ViewModels/WorkspaceViewModel.cs index dc59c93..751a701 100644 --- a/src/EgoPssgEditor/ViewModel/WorkspaceViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/WorkspaceViewModel.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace EgoPssgEditor.ViewModel +namespace EgoPssgEditor.ViewModels { public abstract class WorkspaceViewModel : ViewModelBase { diff --git a/src/EgoPssgEditor/AddAttributeWindow.xaml b/src/EgoPssgEditor/Views/AddAttributeWindow.axaml similarity index 89% rename from src/EgoPssgEditor/AddAttributeWindow.xaml rename to src/EgoPssgEditor/Views/AddAttributeWindow.axaml index ef161ad..b6dd4fe 100644 --- a/src/EgoPssgEditor/AddAttributeWindow.xaml +++ b/src/EgoPssgEditor/Views/AddAttributeWindow.axaml @@ -1,11 +1,11 @@ - + Title="Add Attribute" Height="210" Width="300" CanResize="False" CanMinimize="False"> diff --git a/src/EgoPssgEditor/AddAttributeWindow.xaml.cs b/src/EgoPssgEditor/Views/AddAttributeWindow.axaml.cs similarity index 80% rename from src/EgoPssgEditor/AddAttributeWindow.xaml.cs rename to src/EgoPssgEditor/Views/AddAttributeWindow.axaml.cs index 6e48ed0..d8485bd 100644 --- a/src/EgoPssgEditor/AddAttributeWindow.xaml.cs +++ b/src/EgoPssgEditor/Views/AddAttributeWindow.axaml.cs @@ -1,19 +1,9 @@ using EgoEngineLibrary.Graphics; -using System; -using System.Collections.Generic; -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.Shapes; -namespace EgoPssgEditor +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace EgoPssgEditor.Views { /// /// Interaction logic for AddAttributeWindow.xaml @@ -46,11 +36,11 @@ private void okButton_Click(object sender, RoutedEventArgs e) try { Convert.ChangeType(Value, AttributeValueType); - this.DialogResult = true; + Close(true); } catch { - this.DialogResult = null; + Close(null); } } diff --git a/src/EgoPssgEditor/AddNodeWindow.xaml b/src/EgoPssgEditor/Views/AddNodeWindow.axaml similarity index 82% rename from src/EgoPssgEditor/AddNodeWindow.xaml rename to src/EgoPssgEditor/Views/AddNodeWindow.axaml index 932818b..cbc93eb 100644 --- a/src/EgoPssgEditor/AddNodeWindow.xaml +++ b/src/EgoPssgEditor/Views/AddNodeWindow.axaml @@ -1,11 +1,10 @@ - + Title="Add Node" Height="150" Width="300" CanResize="False" CanMinimize="False"> diff --git a/src/EgoPssgEditor/AddNodeWindow.xaml.cs b/src/EgoPssgEditor/Views/AddNodeWindow.axaml.cs similarity index 61% rename from src/EgoPssgEditor/AddNodeWindow.xaml.cs rename to src/EgoPssgEditor/Views/AddNodeWindow.axaml.cs index 0216b9e..450d2f7 100644 --- a/src/EgoPssgEditor/AddNodeWindow.xaml.cs +++ b/src/EgoPssgEditor/Views/AddNodeWindow.axaml.cs @@ -1,19 +1,9 @@ using EgoEngineLibrary.Graphics; -using System; -using System.Collections.Generic; -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.Shapes; -namespace EgoPssgEditor +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace EgoPssgEditor.Views { /// /// Interaction logic for AddNodeWindow.xaml @@ -32,7 +22,7 @@ public AddNodeWindow() private void okButton_Click(object sender, RoutedEventArgs e) { - this.DialogResult = true; + Close(true); } public string NodeName diff --git a/src/EgoPssgEditor/DuplicateTextureWindow.xaml b/src/EgoPssgEditor/Views/DuplicateTextureWindow.axaml similarity index 81% rename from src/EgoPssgEditor/DuplicateTextureWindow.xaml rename to src/EgoPssgEditor/Views/DuplicateTextureWindow.axaml index ed84212..6beffd4 100644 --- a/src/EgoPssgEditor/DuplicateTextureWindow.xaml +++ b/src/EgoPssgEditor/Views/DuplicateTextureWindow.axaml @@ -1,11 +1,10 @@ - + Title="Duplicate Texture" Height="150" Width="300" CanResize="False" CanMinimize="False"> diff --git a/src/EgoPssgEditor/DuplicateTextureWindow.xaml.cs b/src/EgoPssgEditor/Views/DuplicateTextureWindow.axaml.cs similarity index 58% rename from src/EgoPssgEditor/DuplicateTextureWindow.xaml.cs rename to src/EgoPssgEditor/Views/DuplicateTextureWindow.axaml.cs index bd13464..df1472b 100644 --- a/src/EgoPssgEditor/DuplicateTextureWindow.xaml.cs +++ b/src/EgoPssgEditor/Views/DuplicateTextureWindow.axaml.cs @@ -1,19 +1,7 @@ -using EgoEngineLibrary.Graphics; -using System; -using System.Collections.Generic; -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.Shapes; +using Avalonia.Controls; +using Avalonia.Interactivity; -namespace EgoPssgEditor +namespace EgoPssgEditor.Views { /// /// Interaction logic for DuplicateTextureWindow.xaml @@ -29,11 +17,11 @@ private void okButton_Click(object sender, RoutedEventArgs e) { if (string.IsNullOrEmpty(TextureName) || string.IsNullOrWhiteSpace(TextureName)) { - this.DialogResult = null; + Close(null); } else { - this.DialogResult = true; + Close(true); } } diff --git a/src/EgoPssgEditor/MainWindow.xaml b/src/EgoPssgEditor/Views/MainWindow.axaml similarity index 100% rename from src/EgoPssgEditor/MainWindow.xaml rename to src/EgoPssgEditor/Views/MainWindow.axaml diff --git a/src/EgoPssgEditor/MainWindow.xaml.cs b/src/EgoPssgEditor/Views/MainWindow.axaml.cs similarity index 100% rename from src/EgoPssgEditor/MainWindow.xaml.cs rename to src/EgoPssgEditor/Views/MainWindow.axaml.cs diff --git a/src/EgoPssgEditor/app.manifest b/src/EgoPssgEditor/app.manifest new file mode 100644 index 0000000..9f9dc31 --- /dev/null +++ b/src/EgoPssgEditor/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + From f1dae320e799ba535e1e65f70a027428b81677fc Mon Sep 17 00:00:00 2001 From: Petar Tasev Date: Sun, 18 Jan 2026 21:25:23 -0800 Subject: [PATCH 2/4] Changes after file moves --- src/EgoPssgEditor/App.axaml | 25 +- src/EgoPssgEditor/App.axaml.cs | 59 +++-- src/EgoPssgEditor/ViewModels/ViewModelBase.cs | 130 +---------- src/EgoPssgEditor/Views/MainWindow.axaml | 218 +++++++----------- src/EgoPssgEditor/Views/MainWindow.axaml.cs | 92 +++++++- 5 files changed, 240 insertions(+), 284 deletions(-) diff --git a/src/EgoPssgEditor/App.axaml b/src/EgoPssgEditor/App.axaml index 902cac5..fb04444 100644 --- a/src/EgoPssgEditor/App.axaml +++ b/src/EgoPssgEditor/App.axaml @@ -1,11 +1,14 @@ - - - - - - - \ No newline at end of file + + + + + + + + + + diff --git a/src/EgoPssgEditor/App.axaml.cs b/src/EgoPssgEditor/App.axaml.cs index 5d2bc92..d292086 100644 --- a/src/EgoPssgEditor/App.axaml.cs +++ b/src/EgoPssgEditor/App.axaml.cs @@ -1,22 +1,51 @@ -using System.Globalization; -using System.Threading; -using System.Windows; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; -namespace EgoPssgEditor +using EgoPssgEditor.ViewModels; +using EgoPssgEditor.Views; + +namespace EgoPssgEditor; + +/// +/// Interaction logic for App.xaml +/// +public partial class App : Application { - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application + public static new App? Current => (App?)Application.Current; + + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() { - public App() + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - var culture = new CultureInfo("en-US"); + // Avoid duplicate validations from both Avalonia and the CommunityToolkit. + // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins + DisableAvaloniaDataAnnotationValidation(); + desktop.MainWindow = new MainWindow + { + DataContext = new MainViewModel(), + }; + } - Thread.CurrentThread.CurrentCulture = culture; - Thread.CurrentThread.CurrentUICulture = culture; - CultureInfo.DefaultThreadCurrentCulture = culture; - CultureInfo.DefaultThreadCurrentUICulture = culture; + base.OnFrameworkInitializationCompleted(); + } + + private void DisableAvaloniaDataAnnotationValidation() + { + // Get an array of plugins to remove + var dataValidationPluginsToRemove = + BindingPlugins.DataValidators.OfType().ToArray(); + + // remove each entry found + foreach (var plugin in dataValidationPluginsToRemove) + { + BindingPlugins.DataValidators.Remove(plugin); } } -} \ No newline at end of file +} diff --git a/src/EgoPssgEditor/ViewModels/ViewModelBase.cs b/src/EgoPssgEditor/ViewModels/ViewModelBase.cs index 9fd84a5..b3fa912 100644 --- a/src/EgoPssgEditor/ViewModels/ViewModelBase.cs +++ b/src/EgoPssgEditor/ViewModels/ViewModelBase.cs @@ -1,123 +1,13 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; -namespace EgoPssgEditor.ViewModel -{ - public abstract class ViewModelBase : INotifyPropertyChanged, IDisposable - { - #region Constructor - - protected ViewModelBase() - { - } - - #endregion // Constructor - - #region DisplayName - - /// - /// Returns the user-friendly name of this object. - /// Child classes can set this property to a new value, - /// or override it to determine the value on-demand. - /// - public virtual string DisplayName { get; protected set; } - - #endregion // DisplayName - - #region Debugging Aides - - /// - /// Warns the developer if this object does not have - /// a public property with the specified name. This - /// method does not exist in a Release build. - /// - [Conditional("DEBUG")] - [DebuggerStepThrough] - public void VerifyPropertyName(string propertyName) - { - // Verify that the property name matches a real, - // public, instance property on this object. - if (TypeDescriptor.GetProperties(this)[propertyName] == null) - { - string msg = "Invalid property name: " + propertyName; - - if (this.ThrowOnInvalidPropertyName) - throw new Exception(msg); - else - Debug.Fail(msg); - } - } - - /// - /// Returns whether an exception is thrown, or if a Debug.Fail() is used - /// when an invalid property name is passed to the VerifyPropertyName method. - /// The default value is false, but subclasses used by unit tests might - /// override this property's getter to return true. - /// - protected virtual bool ThrowOnInvalidPropertyName { get; private set; } - - #endregion // Debugging Aides +namespace EgoPssgEditor.ViewModels; - #region INotifyPropertyChanged Members - - /// - /// Raised when a property on this object has a new value. - /// - public event PropertyChangedEventHandler PropertyChanged; - - /// - /// Raises this object's PropertyChanged event. - /// - /// The property that has a new value. - protected virtual void OnPropertyChanged(string propertyName) - { - this.VerifyPropertyName(propertyName); - - PropertyChangedEventHandler handler = this.PropertyChanged; - if (handler != null) - { - var e = new PropertyChangedEventArgs(propertyName); - handler(this, e); - } - } - - #endregion // INotifyPropertyChanged Members - - #region IDisposable Members - - /// - /// Invoked when this object is being removed from the application - /// and will be subject to garbage collection. - /// - public void Dispose() - { - this.OnDispose(); - } - - /// - /// Child classes can override this method to perform - /// clean-up logic, such as removing event handlers. - /// - protected virtual void OnDispose() - { - } - -#if DEBUG - /// - /// Useful for ensuring that ViewModel objects are properly garbage collected. - /// - ~ViewModelBase() - { - string msg = string.Format("{0} ({1}) ({2}) Finalized", this.GetType().Name, this.DisplayName, this.GetHashCode()); - System.Diagnostics.Debug.WriteLine(msg); - } -#endif - - #endregion // IDisposable Members - } +public abstract class ViewModelBase : ObservableObject +{ + /// + /// Returns the user-friendly name of this object. + /// Child classes can set this property to a new value, + /// or override it to determine the value on-demand. + /// + public virtual string DisplayName { get; protected set; } = string.Empty; } diff --git a/src/EgoPssgEditor/Views/MainWindow.axaml b/src/EgoPssgEditor/Views/MainWindow.axaml index 62cfca3..f8e9278 100644 --- a/src/EgoPssgEditor/Views/MainWindow.axaml +++ b/src/EgoPssgEditor/Views/MainWindow.axaml @@ -1,80 +1,83 @@ - - - - - + xmlns:vm="using:EgoPssgEditor.ViewModels" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600" + x:DataType="vm:MainViewModel" + Icon="/Assets/Ryder25.ico" + Title="{Binding DisplayName}" Height="600" Width="800"> + + + + + + + + - + - + - + - + - + - + - - - - - - - - - + + + + + + + + + - - - - - - + + + + + + - - + + - - - + + + - - + + - - - + + + @@ -83,9 +86,9 @@ - - - + + + @@ -93,87 +96,48 @@ - - - + - + - + - - - - - - - - - - - - - - - - - - - + - + - - - - - - - + + + + - + - - + + @@ -181,30 +145,24 @@ - - - - + - + - - + + - + @@ -214,13 +172,13 @@ - - + + - diff --git a/src/EgoPssgEditor/Views/MainWindow.axaml.cs b/src/EgoPssgEditor/Views/MainWindow.axaml.cs index 257886a..271535d 100644 --- a/src/EgoPssgEditor/Views/MainWindow.axaml.cs +++ b/src/EgoPssgEditor/Views/MainWindow.axaml.cs @@ -1,9 +1,14 @@ -using EgoPssgEditor.ViewModel; -using System; +using EgoPssgEditor.ViewModels; + using System.Diagnostics; -using System.Windows; -namespace EgoPssgEditor +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; + +using EgoEngineLibrary.Avalonia; + +namespace EgoPssgEditor.Views { // Copy/Paste; Search; ModelViewer; // 1.7.2 - Quick Float4 Fix To Make work with all textures @@ -22,21 +27,92 @@ namespace EgoPssgEditor /// public partial class MainWindow : Window { - readonly MainViewModel view; + private MainViewModel? view; + private IDisposable? _fileOpenInteractionRegistration; + private IDisposable? _fileSaveInteractionRegistration; + public MainWindow() { InitializeComponent(); - view = this.DataContext as MainViewModel; } - private void websiteMenuItem_Click(object sender, System.Windows.RoutedEventArgs e) + private void websiteMenuItem_Click(object sender, RoutedEventArgs e) { Process.Start(new ProcessStartInfo("https://petar.page/l/ego-epe-home") { UseShellExecute = true }); } - private void sourceCodeMenuItem_Click(object sender, System.Windows.RoutedEventArgs e) + private void sourceCodeMenuItem_Click(object sender, RoutedEventArgs e) { Process.Start(new ProcessStartInfo("https://petar.page/l/ego-epe-code") { UseShellExecute = true }); } + + protected override void OnDataContextChanged(EventArgs e) + { + view = this.DataContext as MainViewModel; + + // Dispose any old handler + _fileOpenInteractionRegistration?.Dispose(); + _fileSaveInteractionRegistration?.Dispose(); + + if (view is not null) + { + // register the interaction handler + _fileOpenInteractionRegistration = view.FileOpenInteraction.RegisterHandler(FileOpenHandler); + _fileSaveInteractionRegistration = view.FileSaveInteraction.RegisterHandler(FileSaveHandler); + } + + base.OnDataContextChanged(e); + } + + private async Task FileOpenHandler(FileOpenOptions openOptions) + { + // Get a reference to our TopLevel (in our case the parent Window) + var topLevel = GetTopLevel(this); + if (topLevel is null) + { + return null; + } + + var options = new FilePickerOpenOptions + { + Title = openOptions.Title, + FileTypeFilter = openOptions.FileTypeChoices, + SuggestedFileType = openOptions.SuggestedFileType, + AllowMultiple = openOptions.AllowMultiple, + SuggestedFileName = openOptions.FileName, + SuggestedStartLocation = openOptions.InitialDirectory is null + ? null + : await topLevel.StorageProvider.TryGetFolderFromPathAsync(openOptions.InitialDirectory), + }; + + var storageFiles = await topLevel.StorageProvider.OpenFilePickerAsync(options); + return storageFiles.Count <= 0 ? null : storageFiles[0].Path.LocalPath; + } + + private async Task FileSaveHandler(FileSaveOptions saveOptions) + { + // Get a reference to our TopLevel (in our case the parent Window) + var topLevel = GetTopLevel(this); + if (topLevel is null) + { + return null; + } + + var options = new FilePickerSaveOptions + { + Title = saveOptions.Title, + FileTypeChoices = saveOptions.FileTypeChoices, + SuggestedFileType = saveOptions.SuggestedFileType, + DefaultExtension = saveOptions.DefaultExtension, + ShowOverwritePrompt = saveOptions.ShowOverwritePrompt, + SuggestedFileName = saveOptions.FileName, + SuggestedStartLocation = saveOptions.InitialDirectory is null + ? null + : await topLevel.StorageProvider.TryGetFolderFromPathAsync(saveOptions.InitialDirectory), + }; + + var storageFiles = await topLevel.StorageProvider.SaveFilePickerAsync(options); + return storageFiles?.Path.LocalPath; + } } } From 9934a015abdbf2ad66769ab04d67aaf71f7ce5f8 Mon Sep 17 00:00:00 2001 From: Petar Tasev Date: Sat, 31 Jan 2026 18:17:16 -0800 Subject: [PATCH 3/4] Refactor dialogs --- EgoEngineLibrary.slnx | 3 +- .../FilePickerOptions.cs | 16 ---- .../FilePickerTypes.cs | 34 -------- .../MessageBox/MessageBox.cs | 52 ----------- .../Dialogs/File/FileDialogAvalonia.cs | 85 ++++++++++++++++++ .../Dialogs/File/FilePickerTypes.cs | 48 ++++++++++ .../Dialogs/MessageBox/MessageBoxAvalonia.cs | 49 +++++++++++ .../MessageBox/MessageBoxWindow.axaml | 2 +- .../MessageBox/MessageBoxWindow.axaml.cs | 2 +- .../EgoEngineLibrary.Frontend.Avalonia.csproj | 19 ++++ .../WindowExtensions.cs} | 4 +- .../Dialogs/File/FileDialog.cs | 18 ++++ .../Dialogs/File/FileOpenMessage.cs | 13 +++ .../Dialogs/File}/FileOpenOptions.cs | 2 +- .../Dialogs/File/FilePickerOptions.cs | 14 +++ .../Dialogs/File/FilePickerType.cs | 14 +++ .../Dialogs/File/FileSaveMessage.cs | 13 +++ .../Dialogs/File}/FileSaveOptions.cs | 2 +- .../Dialogs/MessageBox/MessageBox.cs | 18 ++++ .../Dialogs}/MessageBox/MessageBoxButton.cs | 2 +- .../Dialogs}/MessageBox/MessageBoxImage.cs | 2 +- .../Dialogs}/MessageBox/MessageBoxResult.cs | 2 +- .../MessageBox/MessageBoxShowMessage.cs | 30 +++++++ .../EgoEngineLibrary.Frontend.csproj} | 2 +- .../Messaging/Messenger.cs | 8 ++ .../Dialogs/Pssg/AddAttributeMessage.cs | 5 ++ .../Dialogs/Pssg/AddAttributeResponse.cs | 8 ++ .../Dialogs/Pssg/AddNodeMessage.cs | 5 ++ .../Dialogs/Pssg/DuplicateTextureMessage.cs | 8 ++ .../Dialogs/Pssg/DuplicateTextureViewModel.cs | 6 ++ src/EgoPssgEditor/Dialogs/Pssg/PssgDialog.cs | 23 +++++ .../Dialogs/Pssg/PssgDialogAvalonia.cs | 72 +++++++++++++++ src/EgoPssgEditor/EgoPssgEditor.csproj | 12 +-- src/EgoPssgEditor/Models/Interaction.cs | 58 ------------- src/EgoPssgEditor/ViewModels/MainViewModel.cs | 40 +++------ .../ViewModels/ModelsWorkspaceViewModel.cs | 59 ++++++------- .../ViewModels/NodesWorkspaceViewModel.cs | 52 +++++------ .../ViewModels/PssgTextureViewModel.cs | 6 +- .../ViewModels/TexturesWorkspaceViewModel.cs | 40 ++++----- src/EgoPssgEditor/Views/MainWindow.axaml | 6 +- src/EgoPssgEditor/Views/MainWindow.axaml.cs | 87 +++---------------- 41 files changed, 564 insertions(+), 377 deletions(-) delete mode 100644 src/EgoEngineLibrary.Avalonia/FilePickerOptions.cs delete mode 100644 src/EgoEngineLibrary.Avalonia/FilePickerTypes.cs delete mode 100644 src/EgoEngineLibrary.Avalonia/MessageBox/MessageBox.cs create mode 100644 src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/File/FileDialogAvalonia.cs create mode 100644 src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/File/FilePickerTypes.cs create mode 100644 src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/MessageBox/MessageBoxAvalonia.cs rename src/{EgoEngineLibrary.Avalonia => EgoEngineLibrary.Frontend.Avalonia/Dialogs}/MessageBox/MessageBoxWindow.axaml (98%) rename src/{EgoEngineLibrary.Avalonia => EgoEngineLibrary.Frontend.Avalonia/Dialogs}/MessageBox/MessageBoxWindow.axaml.cs (99%) create mode 100644 src/EgoEngineLibrary.Frontend.Avalonia/EgoEngineLibrary.Frontend.Avalonia.csproj rename src/{EgoEngineLibrary.Avalonia/DialogExtensions.cs => EgoEngineLibrary.Frontend.Avalonia/WindowExtensions.cs} (88%) create mode 100644 src/EgoEngineLibrary.Frontend/Dialogs/File/FileDialog.cs create mode 100644 src/EgoEngineLibrary.Frontend/Dialogs/File/FileOpenMessage.cs rename src/{EgoEngineLibrary.Avalonia => EgoEngineLibrary.Frontend/Dialogs/File}/FileOpenOptions.cs (65%) create mode 100644 src/EgoEngineLibrary.Frontend/Dialogs/File/FilePickerOptions.cs create mode 100644 src/EgoEngineLibrary.Frontend/Dialogs/File/FilePickerType.cs create mode 100644 src/EgoEngineLibrary.Frontend/Dialogs/File/FileSaveMessage.cs rename src/{EgoEngineLibrary.Avalonia => EgoEngineLibrary.Frontend/Dialogs/File}/FileSaveOptions.cs (75%) create mode 100644 src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBox.cs rename src/{EgoEngineLibrary.Avalonia => EgoEngineLibrary.Frontend/Dialogs}/MessageBox/MessageBoxButton.cs (92%) rename src/{EgoEngineLibrary.Avalonia => EgoEngineLibrary.Frontend/Dialogs}/MessageBox/MessageBoxImage.cs (96%) rename src/{EgoEngineLibrary.Avalonia => EgoEngineLibrary.Frontend/Dialogs}/MessageBox/MessageBoxResult.cs (95%) create mode 100644 src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBoxShowMessage.cs rename src/{EgoEngineLibrary.Avalonia/EgoEngineLibrary.Avalonia.csproj => EgoEngineLibrary.Frontend/EgoEngineLibrary.Frontend.csproj} (67%) create mode 100644 src/EgoEngineLibrary.Frontend/Messaging/Messenger.cs create mode 100644 src/EgoPssgEditor/Dialogs/Pssg/AddAttributeMessage.cs create mode 100644 src/EgoPssgEditor/Dialogs/Pssg/AddAttributeResponse.cs create mode 100644 src/EgoPssgEditor/Dialogs/Pssg/AddNodeMessage.cs create mode 100644 src/EgoPssgEditor/Dialogs/Pssg/DuplicateTextureMessage.cs create mode 100644 src/EgoPssgEditor/Dialogs/Pssg/DuplicateTextureViewModel.cs create mode 100644 src/EgoPssgEditor/Dialogs/Pssg/PssgDialog.cs create mode 100644 src/EgoPssgEditor/Dialogs/Pssg/PssgDialogAvalonia.cs delete mode 100644 src/EgoPssgEditor/Models/Interaction.cs diff --git a/EgoEngineLibrary.slnx b/EgoEngineLibrary.slnx index d9f7ee4..177b167 100644 --- a/EgoEngineLibrary.slnx +++ b/EgoEngineLibrary.slnx @@ -11,7 +11,8 @@ - + + diff --git a/src/EgoEngineLibrary.Avalonia/FilePickerOptions.cs b/src/EgoEngineLibrary.Avalonia/FilePickerOptions.cs deleted file mode 100644 index b14f1a7..0000000 --- a/src/EgoEngineLibrary.Avalonia/FilePickerOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia.Platform.Storage; - -namespace EgoEngineLibrary.Avalonia; - -public record FilePickerOptions -{ - public string? Title { get; set; } - - public string? InitialDirectory { get; set; } - - public string? FileName { get; set; } - - public IReadOnlyList? FileTypeChoices { get; set; } - - public FilePickerFileType? SuggestedFileType { get; set; } -} diff --git a/src/EgoEngineLibrary.Avalonia/FilePickerTypes.cs b/src/EgoEngineLibrary.Avalonia/FilePickerTypes.cs deleted file mode 100644 index e60d22a..0000000 --- a/src/EgoEngineLibrary.Avalonia/FilePickerTypes.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Avalonia.Platform.Storage; - -namespace EgoEngineLibrary.Avalonia; - -public static class FilePickerTypes -{ - public static readonly FilePickerFileType Pssg = new("PSSG files") - { - Patterns = ["*.pssg"], - MimeTypes = ["application/octet-stream"], - AppleUniformTypeIdentifiers = ["public.data"] - }; - - public static readonly FilePickerFileType Dds = new("DDS files") - { - Patterns = ["*.dds"], - MimeTypes = ["application/octet-stream"], - AppleUniformTypeIdentifiers = ["public.image"] - }; - - public static readonly FilePickerFileType Gltf = new("Gltf files") - { - Patterns = ["*.glb", "*.gltf"], - MimeTypes = ["model/gltf-binary", "model/gltf+json"], - AppleUniformTypeIdentifiers = ["public.3d-content"] - }; - - public static readonly FilePickerFileType Bin = new("Bin files") - { - Patterns = ["*.bin"], - MimeTypes = ["application/octet-stream"], - AppleUniformTypeIdentifiers = ["public.data"] - }; -} diff --git a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBox.cs b/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBox.cs deleted file mode 100644 index 75de821..0000000 --- a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBox.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; - -namespace EgoEngineLibrary.Avalonia.MessageBox; - -public sealed class MessageBox -{ - public static Task Show( - string messageBoxText, - string caption, - MessageBoxButton button = MessageBoxButton.OK, - MessageBoxImage icon = MessageBoxImage.None, - MessageBoxResult defaultResult = MessageBoxResult.None) - { - if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - return Show(desktop.MainWindow!, messageBoxText, caption, button, icon, defaultResult); - } - else if (Application.Current?.ApplicationLifetime is null) - { - return Task.FromResult(MessageBoxResult.None); - } - - throw new InvalidOperationException("Operation not supported on current application lifetime."); - } - - public static Task Show( - Window owner, - string messageBoxText, - string caption, - MessageBoxButton button = MessageBoxButton.OK, - MessageBoxImage icon = MessageBoxImage.None, - MessageBoxResult defaultResult = MessageBoxResult.None) - { - ArgumentNullException.ThrowIfNull(owner); - - MessageBoxWindow box = new() - { - MinWidth = 114, - MinHeight = 94, - MaxWidth = owner.Width, - MaxHeight = owner.Height, - Title = caption, - Message = messageBoxText, - Buttons = button, - ImageIcon = icon, - DefaultResult = defaultResult - }; - return box.ShowDialog(); - } -} diff --git a/src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/File/FileDialogAvalonia.cs b/src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/File/FileDialogAvalonia.cs new file mode 100644 index 0000000..ed61fac --- /dev/null +++ b/src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/File/FileDialogAvalonia.cs @@ -0,0 +1,85 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Platform.Storage; + +using CommunityToolkit.Mvvm.Messaging; + +namespace EgoEngineLibrary.Frontend.Dialogs.File; + +public static class FileDialogAvalonia +{ + public static void Register(Visual recipient) + { + FileDialog.Messenger.Register(recipient, FileOpenHandler); + FileDialog.Messenger.Register(recipient, FileSaveHandler); + } + + public static void Unregister(Visual recipient) + { + FileDialog.Messenger.Unregister(recipient); + FileDialog.Messenger.Unregister(recipient); + } + + private static void FileOpenHandler(Visual recipient, FileOpenMessage message) + { + message.Reply(FileOpen(recipient, message)); + } + + private static async Task> FileOpen(Visual recipient, FileOpenMessage message) + { + // Get a reference to our TopLevel (in our case the parent Window) + var topLevel = TopLevel.GetTopLevel(recipient); + if (topLevel is null) + { + return []; + } + + var openOptions = message.Options; + var options = new FilePickerOpenOptions + { + Title = openOptions.Title, + FileTypeFilter = openOptions.FileTypeChoices?.Select(x => x.ToFilePickerType()).ToArray(), + SuggestedFileType = openOptions.SuggestedFileType?.ToFilePickerType(), + AllowMultiple = openOptions.AllowMultiple, + SuggestedFileName = openOptions.FileName, + SuggestedStartLocation = openOptions.InitialDirectory is null + ? null + : await topLevel.StorageProvider.TryGetFolderFromPathAsync(openOptions.InitialDirectory), + }; + + var storageFiles = await topLevel.StorageProvider.OpenFilePickerAsync(options); + return storageFiles.Select(x => x.Path.LocalPath).ToArray(); + } + + private static void FileSaveHandler(Visual recipient, FileSaveMessage message) + { + message.Reply(FileSave(recipient, message)); + } + + private static async Task FileSave(Visual recipient, FileSaveMessage message) + { + // Get a reference to our TopLevel (in our case the parent Window) + var topLevel = TopLevel.GetTopLevel(recipient); + if (topLevel is null) + { + return null; + } + + var saveOptions = message.Options; + var options = new FilePickerSaveOptions + { + Title = saveOptions.Title, + FileTypeChoices = saveOptions.FileTypeChoices?.Select(x => x.ToFilePickerType()).ToArray(), + SuggestedFileType = saveOptions.SuggestedFileType?.ToFilePickerType(), + DefaultExtension = saveOptions.DefaultExtension, + ShowOverwritePrompt = saveOptions.ShowOverwritePrompt, + SuggestedFileName = saveOptions.FileName, + SuggestedStartLocation = saveOptions.InitialDirectory is null + ? null + : await topLevel.StorageProvider.TryGetFolderFromPathAsync(saveOptions.InitialDirectory), + }; + + var storageFiles = await topLevel.StorageProvider.SaveFilePickerAsync(options); + return storageFiles?.Path.LocalPath; + } +} diff --git a/src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/File/FilePickerTypes.cs b/src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/File/FilePickerTypes.cs new file mode 100644 index 0000000..096004f --- /dev/null +++ b/src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/File/FilePickerTypes.cs @@ -0,0 +1,48 @@ +using Avalonia.Platform.Storage; + +namespace EgoEngineLibrary.Frontend.Dialogs.File; + +public static class FilePickerTypes +{ + private static readonly FilePickerFileType Pssg = new("PSSG files") + { + Patterns = ["*.pssg"], + MimeTypes = ["application/octet-stream"], + AppleUniformTypeIdentifiers = ["public.data"] + }; + + private static readonly FilePickerFileType Dds = new("DDS files") + { + Patterns = ["*.dds"], + MimeTypes = ["application/octet-stream"], + AppleUniformTypeIdentifiers = ["public.image"] + }; + + private static readonly FilePickerFileType Gltf = new("Gltf files") + { + Patterns = ["*.glb", "*.gltf"], + MimeTypes = ["model/gltf-binary", "model/gltf+json"], + AppleUniformTypeIdentifiers = ["public.3d-content"] + }; + + private static readonly FilePickerFileType Bin = new("Bin files") + { + Patterns = ["*.bin"], + MimeTypes = ["application/octet-stream"], + AppleUniformTypeIdentifiers = ["public.data"] + }; + + public static FilePickerFileType ToFilePickerType(this FilePickerType type) + { + return type switch + { + FilePickerType.All => FilePickerFileTypes.All, + FilePickerType.Bin => Bin, + FilePickerType.Dds => Dds, + FilePickerType.Gltf => Gltf, + FilePickerType.Pssg => Pssg, + FilePickerType.Xml => FilePickerFileTypes.Xml, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } +} diff --git a/src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/MessageBox/MessageBoxAvalonia.cs b/src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/MessageBox/MessageBoxAvalonia.cs new file mode 100644 index 0000000..c464edf --- /dev/null +++ b/src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/MessageBox/MessageBoxAvalonia.cs @@ -0,0 +1,49 @@ +using Avalonia.Controls; + +using CommunityToolkit.Mvvm.Messaging; + +namespace EgoEngineLibrary.Frontend.Dialogs.MessageBox; + +public static class MessageBoxAvalonia +{ + public static void Register(Window recipient) + { + MessageBox.Messenger.Register(recipient, MessageBoxShowHandler); + } + + public static void Unregister(Window recipient) + { + MessageBox.Messenger.Unregister(recipient); + } + + private static void MessageBoxShowHandler(Window recipient, MessageBoxShowMessage message) + { + message.Reply(Show(recipient, message.MessageBoxText, message.Caption, message.Button, message.Icon, + message.DefaultResult)); + } + + private static Task Show( + Window owner, + string messageBoxText, + string caption, + MessageBoxButton button = MessageBoxButton.OK, + MessageBoxImage icon = MessageBoxImage.None, + MessageBoxResult defaultResult = MessageBoxResult.None) + { + ArgumentNullException.ThrowIfNull(owner); + + MessageBoxWindow box = new() + { + MinWidth = 114, + MinHeight = 94, + MaxWidth = owner.Width, + MaxHeight = owner.Height, + Title = caption, + Message = messageBoxText, + Buttons = button, + ImageIcon = icon, + DefaultResult = defaultResult + }; + return box.ShowDialog(owner); + } +} diff --git a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxWindow.axaml b/src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/MessageBox/MessageBoxWindow.axaml similarity index 98% rename from src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxWindow.axaml rename to src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/MessageBox/MessageBoxWindow.axaml index eb67e6a..504a7e9 100644 --- a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxWindow.axaml +++ b/src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/MessageBox/MessageBoxWindow.axaml @@ -3,7 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480" - x:Class="EgoEngineLibrary.Avalonia.MessageBox.MessageBoxWindow" + x:Class="EgoEngineLibrary.Frontend.Dialogs.MessageBox.MessageBoxWindow" Title="MessageBoxWindow" CanResize="False" CanMinimize="False" ShowInTaskbar="false" SizeToContent="WidthAndHeight" Padding="10" WindowStartupLocation="CenterOwner"> diff --git a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxWindow.axaml.cs b/src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/MessageBox/MessageBoxWindow.axaml.cs similarity index 99% rename from src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxWindow.axaml.cs rename to src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/MessageBox/MessageBoxWindow.axaml.cs index adbe3c5..fe21070 100644 --- a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxWindow.axaml.cs +++ b/src/EgoEngineLibrary.Frontend.Avalonia/Dialogs/MessageBox/MessageBoxWindow.axaml.cs @@ -2,7 +2,7 @@ using Avalonia.Interactivity; using Avalonia.Media; -namespace EgoEngineLibrary.Avalonia.MessageBox; +namespace EgoEngineLibrary.Frontend.Dialogs.MessageBox; internal partial class MessageBoxWindow : Window { diff --git a/src/EgoEngineLibrary.Frontend.Avalonia/EgoEngineLibrary.Frontend.Avalonia.csproj b/src/EgoEngineLibrary.Frontend.Avalonia/EgoEngineLibrary.Frontend.Avalonia.csproj new file mode 100644 index 0000000..a5fb5c4 --- /dev/null +++ b/src/EgoEngineLibrary.Frontend.Avalonia/EgoEngineLibrary.Frontend.Avalonia.csproj @@ -0,0 +1,19 @@ + + + + EgoEngineLibrary.Frontend + + + + + + + + + + + + + + + diff --git a/src/EgoEngineLibrary.Avalonia/DialogExtensions.cs b/src/EgoEngineLibrary.Frontend.Avalonia/WindowExtensions.cs similarity index 88% rename from src/EgoEngineLibrary.Avalonia/DialogExtensions.cs rename to src/EgoEngineLibrary.Frontend.Avalonia/WindowExtensions.cs index 7444aed..c128859 100644 --- a/src/EgoEngineLibrary.Avalonia/DialogExtensions.cs +++ b/src/EgoEngineLibrary.Frontend.Avalonia/WindowExtensions.cs @@ -2,9 +2,9 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; -namespace EgoEngineLibrary.Avalonia; +namespace EgoEngineLibrary.Frontend; -public static class DialogExtensions +public static class WindowExtensions { public static Task ShowDialog(this Window window) { diff --git a/src/EgoEngineLibrary.Frontend/Dialogs/File/FileDialog.cs b/src/EgoEngineLibrary.Frontend/Dialogs/File/FileDialog.cs new file mode 100644 index 0000000..c83912c --- /dev/null +++ b/src/EgoEngineLibrary.Frontend/Dialogs/File/FileDialog.cs @@ -0,0 +1,18 @@ +using CommunityToolkit.Mvvm.Messaging; + +namespace EgoEngineLibrary.Frontend.Dialogs.File; + +public class FileDialog +{ + public static IMessenger Messenger { get; set; } = Messaging.Messenger.Default; + + public static async Task> ShowOpenFileDialog(FileOpenOptions openOptions) + { + return await Messenger.Send(new FileOpenMessage(openOptions)); + } + + public static async Task ShowSaveFileDialog(FileSaveOptions saveOptions) + { + return await Messenger.Send(new FileSaveMessage(saveOptions)); + } +} diff --git a/src/EgoEngineLibrary.Frontend/Dialogs/File/FileOpenMessage.cs b/src/EgoEngineLibrary.Frontend/Dialogs/File/FileOpenMessage.cs new file mode 100644 index 0000000..893d19d --- /dev/null +++ b/src/EgoEngineLibrary.Frontend/Dialogs/File/FileOpenMessage.cs @@ -0,0 +1,13 @@ +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace EgoEngineLibrary.Frontend.Dialogs.File; + +public class FileOpenMessage : AsyncRequestMessage> +{ + public FileOpenOptions Options { get; } + + public FileOpenMessage(FileOpenOptions options) + { + Options = options; + } +} diff --git a/src/EgoEngineLibrary.Avalonia/FileOpenOptions.cs b/src/EgoEngineLibrary.Frontend/Dialogs/File/FileOpenOptions.cs similarity index 65% rename from src/EgoEngineLibrary.Avalonia/FileOpenOptions.cs rename to src/EgoEngineLibrary.Frontend/Dialogs/File/FileOpenOptions.cs index c09f57c..7a144ea 100644 --- a/src/EgoEngineLibrary.Avalonia/FileOpenOptions.cs +++ b/src/EgoEngineLibrary.Frontend/Dialogs/File/FileOpenOptions.cs @@ -1,4 +1,4 @@ -namespace EgoEngineLibrary.Avalonia; +namespace EgoEngineLibrary.Frontend.Dialogs.File; public record FileOpenOptions : FilePickerOptions { diff --git a/src/EgoEngineLibrary.Frontend/Dialogs/File/FilePickerOptions.cs b/src/EgoEngineLibrary.Frontend/Dialogs/File/FilePickerOptions.cs new file mode 100644 index 0000000..37620f0 --- /dev/null +++ b/src/EgoEngineLibrary.Frontend/Dialogs/File/FilePickerOptions.cs @@ -0,0 +1,14 @@ +namespace EgoEngineLibrary.Frontend.Dialogs.File; + +public record FilePickerOptions +{ + public string? Title { get; set; } + + public string? InitialDirectory { get; set; } + + public string? FileName { get; set; } + + public IReadOnlyList? FileTypeChoices { get; set; } + + public FilePickerType? SuggestedFileType { get; set; } +} diff --git a/src/EgoEngineLibrary.Frontend/Dialogs/File/FilePickerType.cs b/src/EgoEngineLibrary.Frontend/Dialogs/File/FilePickerType.cs new file mode 100644 index 0000000..585e1f0 --- /dev/null +++ b/src/EgoEngineLibrary.Frontend/Dialogs/File/FilePickerType.cs @@ -0,0 +1,14 @@ +namespace EgoEngineLibrary.Frontend.Dialogs.File; + +/// +/// Represents a file type name. +/// +public enum FilePickerType +{ + All, + Bin, + Dds, + Gltf, + Pssg, + Xml, +} diff --git a/src/EgoEngineLibrary.Frontend/Dialogs/File/FileSaveMessage.cs b/src/EgoEngineLibrary.Frontend/Dialogs/File/FileSaveMessage.cs new file mode 100644 index 0000000..36ed3fa --- /dev/null +++ b/src/EgoEngineLibrary.Frontend/Dialogs/File/FileSaveMessage.cs @@ -0,0 +1,13 @@ +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace EgoEngineLibrary.Frontend.Dialogs.File; + +public class FileSaveMessage : AsyncRequestMessage +{ + public FileSaveOptions Options { get; } + + public FileSaveMessage(FileSaveOptions options) + { + Options = options; + } +} diff --git a/src/EgoEngineLibrary.Avalonia/FileSaveOptions.cs b/src/EgoEngineLibrary.Frontend/Dialogs/File/FileSaveOptions.cs similarity index 75% rename from src/EgoEngineLibrary.Avalonia/FileSaveOptions.cs rename to src/EgoEngineLibrary.Frontend/Dialogs/File/FileSaveOptions.cs index 07c18bb..fbddac9 100644 --- a/src/EgoEngineLibrary.Avalonia/FileSaveOptions.cs +++ b/src/EgoEngineLibrary.Frontend/Dialogs/File/FileSaveOptions.cs @@ -1,4 +1,4 @@ -namespace EgoEngineLibrary.Avalonia; +namespace EgoEngineLibrary.Frontend.Dialogs.File; public record FileSaveOptions : FilePickerOptions { diff --git a/src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBox.cs b/src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBox.cs new file mode 100644 index 0000000..41f97eb --- /dev/null +++ b/src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBox.cs @@ -0,0 +1,18 @@ +using CommunityToolkit.Mvvm.Messaging; + +namespace EgoEngineLibrary.Frontend.Dialogs.MessageBox; + +public static class MessageBox +{ + public static IMessenger Messenger { get; set; } = Messaging.Messenger.Default; + + public static async Task Show( + string messageBoxText, + string caption, + MessageBoxButton button = MessageBoxButton.OK, + MessageBoxImage icon = MessageBoxImage.None, + MessageBoxResult defaultResult = MessageBoxResult.None) + { + return await Messenger.Send(new MessageBoxShowMessage(messageBoxText, caption, button, icon, defaultResult)); + } +} diff --git a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxButton.cs b/src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBoxButton.cs similarity index 92% rename from src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxButton.cs rename to src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBoxButton.cs index b590f1b..0e220c8 100644 --- a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxButton.cs +++ b/src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBoxButton.cs @@ -1,4 +1,4 @@ -namespace EgoEngineLibrary.Avalonia.MessageBox; +namespace EgoEngineLibrary.Frontend.Dialogs.MessageBox; /// Specifies the buttons that are displayed on a message box. public enum MessageBoxButton diff --git a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxImage.cs b/src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBoxImage.cs similarity index 96% rename from src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxImage.cs rename to src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBoxImage.cs index 4157a47..0674df0 100644 --- a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxImage.cs +++ b/src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBoxImage.cs @@ -1,4 +1,4 @@ -namespace EgoEngineLibrary.Avalonia.MessageBox; +namespace EgoEngineLibrary.Frontend.Dialogs.MessageBox; /// Specifies the icon that is displayed by a message box. public enum MessageBoxImage diff --git a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxResult.cs b/src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBoxResult.cs similarity index 95% rename from src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxResult.cs rename to src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBoxResult.cs index c950659..bd6aa5b 100644 --- a/src/EgoEngineLibrary.Avalonia/MessageBox/MessageBoxResult.cs +++ b/src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBoxResult.cs @@ -1,4 +1,4 @@ -namespace EgoEngineLibrary.Avalonia.MessageBox; +namespace EgoEngineLibrary.Frontend.Dialogs.MessageBox; /// Specifies which message box button that a user clicks. public enum MessageBoxResult diff --git a/src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBoxShowMessage.cs b/src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBoxShowMessage.cs new file mode 100644 index 0000000..bf00388 --- /dev/null +++ b/src/EgoEngineLibrary.Frontend/Dialogs/MessageBox/MessageBoxShowMessage.cs @@ -0,0 +1,30 @@ +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace EgoEngineLibrary.Frontend.Dialogs.MessageBox; + +public class MessageBoxShowMessage : AsyncRequestMessage +{ + public string MessageBoxText { get; } + + public string Caption { get; } + + public MessageBoxButton Button { get; } + + public MessageBoxImage Icon { get; } + + public MessageBoxResult DefaultResult { get; } + + public MessageBoxShowMessage( + string messageBoxText, + string caption, + MessageBoxButton button = MessageBoxButton.OK, + MessageBoxImage icon = MessageBoxImage.None, + MessageBoxResult defaultResult = MessageBoxResult.None) + { + MessageBoxText = messageBoxText; + Caption = caption; + Button = button; + Icon = icon; + DefaultResult = defaultResult; + } +} diff --git a/src/EgoEngineLibrary.Avalonia/EgoEngineLibrary.Avalonia.csproj b/src/EgoEngineLibrary.Frontend/EgoEngineLibrary.Frontend.csproj similarity index 67% rename from src/EgoEngineLibrary.Avalonia/EgoEngineLibrary.Avalonia.csproj rename to src/EgoEngineLibrary.Frontend/EgoEngineLibrary.Frontend.csproj index dd47294..924f753 100644 --- a/src/EgoEngineLibrary.Avalonia/EgoEngineLibrary.Avalonia.csproj +++ b/src/EgoEngineLibrary.Frontend/EgoEngineLibrary.Frontend.csproj @@ -4,7 +4,7 @@ - + diff --git a/src/EgoEngineLibrary.Frontend/Messaging/Messenger.cs b/src/EgoEngineLibrary.Frontend/Messaging/Messenger.cs new file mode 100644 index 0000000..8c75209 --- /dev/null +++ b/src/EgoEngineLibrary.Frontend/Messaging/Messenger.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.Messaging; + +namespace EgoEngineLibrary.Frontend.Messaging; + +public static class Messenger +{ + public static IMessenger Default { get; set; } = WeakReferenceMessenger.Default; +} diff --git a/src/EgoPssgEditor/Dialogs/Pssg/AddAttributeMessage.cs b/src/EgoPssgEditor/Dialogs/Pssg/AddAttributeMessage.cs new file mode 100644 index 0000000..e0bea92 --- /dev/null +++ b/src/EgoPssgEditor/Dialogs/Pssg/AddAttributeMessage.cs @@ -0,0 +1,5 @@ +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace EgoPssgEditor.Dialogs.Pssg; + +public class AddAttributeMessage : AsyncRequestMessage; diff --git a/src/EgoPssgEditor/Dialogs/Pssg/AddAttributeResponse.cs b/src/EgoPssgEditor/Dialogs/Pssg/AddAttributeResponse.cs new file mode 100644 index 0000000..e78e712 --- /dev/null +++ b/src/EgoPssgEditor/Dialogs/Pssg/AddAttributeResponse.cs @@ -0,0 +1,8 @@ +namespace EgoPssgEditor.Dialogs.Pssg; + +public class AddAttributeResponse +{ + public required string Name { get; init; } + public required string Value { get; init; } + public required Type Type { get; init; } +} diff --git a/src/EgoPssgEditor/Dialogs/Pssg/AddNodeMessage.cs b/src/EgoPssgEditor/Dialogs/Pssg/AddNodeMessage.cs new file mode 100644 index 0000000..16ce402 --- /dev/null +++ b/src/EgoPssgEditor/Dialogs/Pssg/AddNodeMessage.cs @@ -0,0 +1,5 @@ +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace EgoPssgEditor.Dialogs.Pssg; + +public class AddNodeMessage : AsyncRequestMessage; diff --git a/src/EgoPssgEditor/Dialogs/Pssg/DuplicateTextureMessage.cs b/src/EgoPssgEditor/Dialogs/Pssg/DuplicateTextureMessage.cs new file mode 100644 index 0000000..60f88d0 --- /dev/null +++ b/src/EgoPssgEditor/Dialogs/Pssg/DuplicateTextureMessage.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace EgoPssgEditor.Dialogs.Pssg; + +public class DuplicateTextureMessage(DuplicateTextureViewModel viewModel) : AsyncRequestMessage +{ + public DuplicateTextureViewModel ViewModel { get; } = viewModel; +} diff --git a/src/EgoPssgEditor/Dialogs/Pssg/DuplicateTextureViewModel.cs b/src/EgoPssgEditor/Dialogs/Pssg/DuplicateTextureViewModel.cs new file mode 100644 index 0000000..3e4a0a6 --- /dev/null +++ b/src/EgoPssgEditor/Dialogs/Pssg/DuplicateTextureViewModel.cs @@ -0,0 +1,6 @@ +namespace EgoPssgEditor.Dialogs.Pssg; + +public class DuplicateTextureViewModel +{ + public string TextureName { get; set; } = string.Empty; +} diff --git a/src/EgoPssgEditor/Dialogs/Pssg/PssgDialog.cs b/src/EgoPssgEditor/Dialogs/Pssg/PssgDialog.cs new file mode 100644 index 0000000..bdefc69 --- /dev/null +++ b/src/EgoPssgEditor/Dialogs/Pssg/PssgDialog.cs @@ -0,0 +1,23 @@ +using CommunityToolkit.Mvvm.Messaging; + +namespace EgoPssgEditor.Dialogs.Pssg; + +public class PssgDialog +{ + public static IMessenger Messenger { get; set; } = EgoEngineLibrary.Frontend.Messaging.Messenger.Default; + + public static async Task ShowAddNodeDialog() + { + return await Messenger.Send(new AddNodeMessage()); + } + + public static async Task ShowAddAttributeDialog() + { + return await Messenger.Send(new AddAttributeMessage()); + } + + public static async Task ShowDuplicateTextureDialog(DuplicateTextureViewModel viewModel) + { + return await Messenger.Send(new DuplicateTextureMessage(viewModel)); + } +} diff --git a/src/EgoPssgEditor/Dialogs/Pssg/PssgDialogAvalonia.cs b/src/EgoPssgEditor/Dialogs/Pssg/PssgDialogAvalonia.cs new file mode 100644 index 0000000..cd26b09 --- /dev/null +++ b/src/EgoPssgEditor/Dialogs/Pssg/PssgDialogAvalonia.cs @@ -0,0 +1,72 @@ +using Avalonia.Controls; + +using CommunityToolkit.Mvvm.Messaging; + +using EgoPssgEditor.Views; + +namespace EgoPssgEditor.Dialogs.Pssg; + +public class PssgDialogAvalonia +{ + public static void Register(Window recipient) + { + PssgDialog.Messenger.Register(recipient, AddNodeHandler); + PssgDialog.Messenger.Register(recipient, AddAttributeHandler); + PssgDialog.Messenger.Register(recipient, DuplicateTextureHandler); + } + + public static void Unregister(Window recipient) + { + PssgDialog.Messenger.Unregister(recipient); + PssgDialog.Messenger.Unregister(recipient); + PssgDialog.Messenger.Unregister(recipient); + } + + private static void AddNodeHandler(Window recipient, AddNodeMessage message) + { + message.Reply(ShowAddNode(recipient)); + return; + + static async Task ShowAddNode(Window owner) + { + ArgumentNullException.ThrowIfNull(owner); + + AddNodeWindow win = new AddNodeWindow(); + var res = await win.ShowDialog(owner); + return res ? win.NodeName : null; + } + } + + private static void AddAttributeHandler(Window recipient, AddAttributeMessage message) + { + message.Reply(ShowAddAttribute(recipient)); + return; + + static async Task ShowAddAttribute(Window owner) + { + ArgumentNullException.ThrowIfNull(owner); + + AddAttributeWindow win = new(); + var res = await win.ShowDialog(owner); + return res + ? new AddAttributeResponse { Name = win.AttributeName, Value = win.Value, Type = win.AttributeValueType, } + : null; + } + } + + private static void DuplicateTextureHandler(Window recipient, DuplicateTextureMessage message) + { + message.Reply(ShowDuplicateTexture(recipient, message.ViewModel)); + return; + + static async Task ShowDuplicateTexture(Window owner, DuplicateTextureViewModel viewModel) + { + ArgumentNullException.ThrowIfNull(owner); + + DuplicateTextureWindow win = new() { TextureName = viewModel.TextureName }; + var res = await win.ShowDialog(owner); + viewModel.TextureName = win.TextureName; + return res; + } + } +} diff --git a/src/EgoPssgEditor/EgoPssgEditor.csproj b/src/EgoPssgEditor/EgoPssgEditor.csproj index baf361c..2eb2dac 100644 --- a/src/EgoPssgEditor/EgoPssgEditor.csproj +++ b/src/EgoPssgEditor/EgoPssgEditor.csproj @@ -33,15 +33,13 @@ Always - + - - - + - + @@ -60,10 +58,6 @@ - - - - True diff --git a/src/EgoPssgEditor/Models/Interaction.cs b/src/EgoPssgEditor/Models/Interaction.cs deleted file mode 100644 index a611af4..0000000 --- a/src/EgoPssgEditor/Models/Interaction.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Windows.Input; - -namespace EgoPssgEditor.Models; - -/// -/// Simple implementation of Interaction pattern from ReactiveUI framework. -/// https://www.reactiveui.net/docs/handbook/interactions/ -/// -public sealed class Interaction : IDisposable, ICommand -{ - // this is a reference to the registered interaction handler. - private Func>? _handler; - - /// - /// Performs the requested interaction . Returns the result provided by the View - /// - /// The input parameter - /// The result of the interaction - /// - public Task HandleAsync(TInput input) - { - if (_handler is null) - { - throw new InvalidOperationException("Handler wasn't registered"); - } - - return _handler(input); - } - - /// - /// Registers a handler to our Interaction - /// - /// the handler to register - /// a disposable object to clean up memory if not in use anymore/> - /// - public IDisposable RegisterHandler(Func> handler) - { - if (_handler is not null) - { - throw new InvalidOperationException("Handler was already registered"); - } - - _handler = handler; - CanExecuteChanged?.Invoke(this, EventArgs.Empty); - return this; - } - - public void Dispose() - { - _handler = null; - } - - public bool CanExecute(object? parameter) => _handler is not null; - - public void Execute(object? parameter) => HandleAsync((TInput?)parameter!); - - public event EventHandler? CanExecuteChanged; -} diff --git a/src/EgoPssgEditor/ViewModels/MainViewModel.cs b/src/EgoPssgEditor/ViewModels/MainViewModel.cs index b66a724..9a1af46 100644 --- a/src/EgoPssgEditor/ViewModels/MainViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/MainViewModel.cs @@ -1,20 +1,9 @@ using EgoEngineLibrary.Graphics; -using Microsoft.Win32; -using System; -using System.IO; -using System.Threading.Tasks; -using System.Windows; -using Avalonia.Controls; -using Avalonia.Platform.Storage; - -using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.Input; -using EgoEngineLibrary.Avalonia; -using EgoEngineLibrary.Avalonia.MessageBox; - -using EgoPssgEditor.Models; +using EgoEngineLibrary.Frontend.Dialogs.File; +using EgoEngineLibrary.Frontend.Dialogs.MessageBox; namespace EgoPssgEditor.ViewModels { @@ -75,9 +64,6 @@ public int SelectedTabIndex } #endregion - public Interaction FileOpenInteraction { get; } = new(); - public Interaction FileSaveInteraction { get; } = new(); - public MainViewModel() { @@ -132,8 +118,8 @@ private async Task Open() FileOpenOptions openOptions = new() { Title = "Open pssg", - FileTypeChoices = [FilePickerTypes.Pssg, FilePickerFileTypes.Xml, FilePickerFileTypes.All], - SuggestedFileType = FilePickerTypes.Pssg, + FileTypeChoices = [FilePickerType.Pssg, FilePickerType.Xml, FilePickerType.All], + SuggestedFileType = FilePickerType.Pssg, }; if (!string.IsNullOrEmpty(filePath)) { @@ -141,12 +127,12 @@ private async Task Open() openOptions.InitialDirectory = Path.GetDirectoryName(filePath); } - var result = await FileOpenInteraction.HandleAsync(openOptions); - if (result is not null) + var result = await FileDialog.ShowOpenFileDialog(openOptions); + if (result.Count > 0) { try { - filePath = result; + filePath = result[0]; using (var fs = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { var pssg = PssgFile.Open(fs); @@ -225,20 +211,20 @@ private async Task SavePssg(PssgFileType type) FileSaveOptions saveOptions = new() { Title = "Save pssg", - FileTypeChoices = [FilePickerTypes.Pssg, FilePickerFileTypes.Xml, FilePickerFileTypes.All], + FileTypeChoices = [FilePickerType.Pssg, FilePickerType.Xml, FilePickerType.All], SuggestedFileType = type switch { - PssgFileType.Pssg => FilePickerTypes.Pssg, - PssgFileType.Xml => FilePickerFileTypes.Xml, - PssgFileType.CompressedPssg => FilePickerTypes.Pssg, - PssgFileType.CompressedXml => FilePickerFileTypes.Xml, + PssgFileType.Pssg => FilePickerType.Pssg, + PssgFileType.Xml => FilePickerType.Xml, + PssgFileType.CompressedPssg => FilePickerType.Pssg, + PssgFileType.CompressedXml => FilePickerType.Xml, _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) }, }; saveOptions.FileName = Path.GetFileNameWithoutExtension(filePath); saveOptions.InitialDirectory = Path.GetDirectoryName(filePath); - var result = await FileSaveInteraction.HandleAsync(saveOptions); + var result = await FileDialog.ShowSaveFileDialog(saveOptions); if (result is not null) { SaveTag(); diff --git a/src/EgoPssgEditor/ViewModels/ModelsWorkspaceViewModel.cs b/src/EgoPssgEditor/ViewModels/ModelsWorkspaceViewModel.cs index d4b4ba2..05552fa 100644 --- a/src/EgoPssgEditor/ViewModels/ModelsWorkspaceViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/ModelsWorkspaceViewModel.cs @@ -1,21 +1,14 @@ using EgoEngineLibrary.Formats.Pssg; using EgoEngineLibrary.Graphics; -using EgoPssgEditor.ViewModels; -using Microsoft.Win32; + using SharpGLTF.Schema2; -using System; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Windows; -using Avalonia.Controls; -using Avalonia.Platform.Storage; +using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.Input; -using EgoEngineLibrary.Avalonia; -using EgoEngineLibrary.Avalonia.MessageBox; +using EgoEngineLibrary.Frontend.Dialogs.File; +using EgoEngineLibrary.Frontend.Dialogs.MessageBox; namespace EgoPssgEditor.ViewModels { @@ -80,7 +73,7 @@ private async Task ExportCar() { FileSaveOptions saveOptions = new() { - FileTypeChoices = [FilePickerTypes.Gltf, FilePickerFileTypes.All], + FileTypeChoices = [FilePickerType.Gltf, FilePickerType.All], Title = "Select the model's save location and file name", DefaultExtension = "glb", }; @@ -90,7 +83,7 @@ private async Task ExportCar() saveOptions.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); } - var result = await mainView.FileSaveInteraction.HandleAsync(saveOptions); + var result = await FileDialog.ShowSaveFileDialog(saveOptions); if (result is not null) { try @@ -120,7 +113,7 @@ private async Task ImportCar() { FileOpenOptions openOptions = new() { - FileTypeChoices = [FilePickerTypes.Gltf, FilePickerFileTypes.All], + FileTypeChoices = [FilePickerType.Gltf, FilePickerType.All], Title = "Select a gltf model file", }; if (!string.IsNullOrEmpty(mainView.FilePath)) @@ -129,12 +122,12 @@ private async Task ImportCar() openOptions.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); } - var result = await mainView.FileOpenInteraction.HandleAsync(openOptions); - if (result is not null) + var result = await FileDialog.ShowOpenFileDialog(openOptions); + if (result.Count > 0) { try { - var gltf = ModelRoot.Load(result); + var gltf = ModelRoot.Load(result[0]); var conv = new GltfCarExteriorPssgConverter(); conv.Convert(gltf, _pssg); @@ -162,7 +155,7 @@ private async Task ExportDirt() { FileSaveOptions saveOptions = new() { - FileTypeChoices = [FilePickerTypes.Gltf, FilePickerFileTypes.All], + FileTypeChoices = [FilePickerType.Gltf, FilePickerType.All], Title = "Select the model's save location and file name", DefaultExtension = "glb", }; @@ -172,7 +165,7 @@ private async Task ExportDirt() saveOptions.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); } - var result = await mainView.FileSaveInteraction.HandleAsync(saveOptions); + var result = await FileDialog.ShowSaveFileDialog(saveOptions); if (result is not null) { try @@ -202,7 +195,7 @@ private async Task ImportDirt() { FileOpenOptions openOptions = new() { - FileTypeChoices = [FilePickerTypes.Gltf, FilePickerFileTypes.All], + FileTypeChoices = [FilePickerType.Gltf, FilePickerType.All], Title = "Select a gltf model file", }; if (!string.IsNullOrEmpty(mainView.FilePath)) @@ -211,12 +204,12 @@ private async Task ImportDirt() openOptions.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); } - var result = await mainView.FileOpenInteraction.HandleAsync(openOptions); - if (result is not null) + var result = await FileDialog.ShowOpenFileDialog(openOptions); + if (result.Count > 0) { try { - var gltf = ModelRoot.Load(result); + var gltf = ModelRoot.Load(result[0]); var conv = new GltfDirtCarExteriorPssgConverter(); conv.Convert(gltf, _pssg); @@ -244,7 +237,7 @@ private async Task ImportGrid() { FileOpenOptions openOptions = new() { - FileTypeChoices = [FilePickerTypes.Gltf, FilePickerFileTypes.All], + FileTypeChoices = [FilePickerType.Gltf, FilePickerType.All], Title = "Select a gltf model file", }; if (!string.IsNullOrEmpty(mainView.FilePath)) @@ -253,12 +246,12 @@ private async Task ImportGrid() openOptions.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); } - var result = await mainView.FileOpenInteraction.HandleAsync(openOptions); - if (result is not null) + var result = await FileDialog.ShowOpenFileDialog(openOptions); + if (result.Count > 0) { try { - var gltf = ModelRoot.Load(result); + var gltf = ModelRoot.Load(result[0]); var conv = new GltfGridCarExteriorPssgConverter(); conv.Convert(gltf, _pssg); @@ -286,7 +279,7 @@ private async Task ExportCarInterior() { FileSaveOptions saveOptions = new() { - FileTypeChoices = [FilePickerTypes.Gltf, FilePickerFileTypes.All], + FileTypeChoices = [FilePickerType.Gltf, FilePickerType.All], Title = "Select the model's save location and file name", DefaultExtension = "glb", }; @@ -296,7 +289,7 @@ private async Task ExportCarInterior() saveOptions.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); } - var result = await mainView.FileSaveInteraction.HandleAsync(saveOptions); + var result = await FileDialog.ShowSaveFileDialog(saveOptions); if (result is not null) { try @@ -327,7 +320,7 @@ private async Task ImportCarInterior() { FileOpenOptions openOptions = new() { - FileTypeChoices = [FilePickerTypes.Gltf, FilePickerFileTypes.All], + FileTypeChoices = [FilePickerType.Gltf, FilePickerType.All], Title = "Select a gltf model file", }; if (!string.IsNullOrEmpty(mainView.FilePath)) @@ -336,12 +329,12 @@ private async Task ImportCarInterior() openOptions.InitialDirectory = Path.GetDirectoryName(mainView.FilePath); } - var result = await mainView.FileOpenInteraction.HandleAsync(openOptions); - if (result is not null) + var result = await FileDialog.ShowOpenFileDialog(openOptions); + if (result.Count > 0) { try { - var gltf = ModelRoot.Load(result); + var gltf = ModelRoot.Load(result[0]); var conv = new GltfCarInteriorPssgConverter(); conv.Convert(gltf, _pssg); diff --git a/src/EgoPssgEditor/ViewModels/NodesWorkspaceViewModel.cs b/src/EgoPssgEditor/ViewModels/NodesWorkspaceViewModel.cs index 1b6a8a9..02aa1cf 100644 --- a/src/EgoPssgEditor/ViewModels/NodesWorkspaceViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/NodesWorkspaceViewModel.cs @@ -1,26 +1,17 @@ using EgoEngineLibrary.Graphics; -using Microsoft.Win32; -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.IO; -using System.Linq; using System.Text; -using System.Threading.Tasks; -using System.Windows; using System.Xml; using System.Xml.Linq; -using Avalonia.Platform.Storage; - using CommunityToolkit.Mvvm.Input; -using EgoEngineLibrary.Avalonia; -using EgoEngineLibrary.Avalonia.MessageBox; using EgoEngineLibrary.Conversion; +using EgoEngineLibrary.Frontend.Dialogs.File; +using EgoEngineLibrary.Frontend.Dialogs.MessageBox; -using EgoPssgEditor.Views; +using EgoPssgEditor.Dialogs.Pssg; namespace EgoPssgEditor.ViewModels { @@ -86,13 +77,13 @@ private async Task Export(object parameter) PssgNode node = ((PssgNodeViewModel)parameter).Node; FileSaveOptions saveOptions = new() { - FileTypeChoices = [FilePickerFileTypes.Xml, FilePickerFileTypes.All], + FileTypeChoices = [FilePickerType.Xml, FilePickerType.All], Title = "Select the node's save location and file name", DefaultExtension = "xml", FileName = "node.xml", }; - var result = await mainView.FileSaveInteraction.HandleAsync(saveOptions); + var result = await FileDialog.ShowSaveFileDialog(saveOptions); if (result is not null) { try @@ -131,18 +122,18 @@ private async Task Import(object parameter) PssgNodeViewModel nodeView = (PssgNodeViewModel)parameter; FileOpenOptions openOptions = new() { - FileTypeChoices = [FilePickerFileTypes.Xml, FilePickerFileTypes.All], + FileTypeChoices = [FilePickerType.Xml, FilePickerType.All], Title = "Select a xml file", FileName = "node.xml", }; - var result = await mainView.FileOpenInteraction.HandleAsync(openOptions); - if (result is not null) + var result = await FileDialog.ShowOpenFileDialog(openOptions); + if (result.Count > 0) { try { PssgNode node = nodeView.Node; - using (FileStream fileStream = File.Open(result, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (FileStream fileStream = File.Open(result[0], FileMode.Open, FileAccess.Read, FileShare.Read)) { XDocument xDoc = XDocument.Load(fileStream); @@ -186,13 +177,13 @@ private async Task ExportData(object parameter) PssgNodeViewModel nodeView = (PssgNodeViewModel)parameter; FileSaveOptions saveOptions = new() { - FileTypeChoices = [FilePickerTypes.Bin, FilePickerFileTypes.All], + FileTypeChoices = [FilePickerType.Bin, FilePickerType.All], Title = "Select the byte data save location and file name", DefaultExtension = "bin", FileName = "nodeData.bin", }; - var result = await mainView.FileSaveInteraction.HandleAsync(saveOptions); + var result = await FileDialog.ShowSaveFileDialog(saveOptions); if (result is not null) { try @@ -223,18 +214,18 @@ private async Task ImportData(object parameter) PssgNodeViewModel nodeView = (PssgNodeViewModel)parameter; FileOpenOptions openOptions = new() { - FileTypeChoices = [FilePickerTypes.Bin, FilePickerFileTypes.All], + FileTypeChoices = [FilePickerType.Bin, FilePickerType.All], Title = "Select a bin file", FileName = "nodeData.bin", }; - var result = await mainView.FileOpenInteraction.HandleAsync(openOptions); - if (result is not null) + var result = await FileDialog.ShowOpenFileDialog(openOptions); + if (result.Count > 0) { try { PssgNode node = nodeView.Node; - using (var fs = File.Open(result, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var fs = File.Open(result[0], FileMode.Open, FileAccess.Read, FileShare.Read)) using (PssgBinaryReader reader = new PssgBinaryReader(EndianBitConverter.Big, fs, false)) { node.Value = reader.ReadNodeValue((int)reader.BaseStream.Length); @@ -264,10 +255,10 @@ private async Task AddNode(object parameter) return; } - AddNodeWindow aaw = new AddNodeWindow(); - if (await aaw.ShowDialog() == true) + var nodeName = await PssgDialog.ShowAddNodeDialog(); + if (nodeName is not null) { - PssgNode newNode = nodeView.Node.AppendChild(aaw.NodeName); + PssgNode newNode = nodeView.Node.AppendChild(nodeName); if (newNode == null) { @@ -328,11 +319,10 @@ private bool AddAttribute_CanExecute(object parameter) private async Task AddAttribute(object parameter) { PssgNodeViewModel nodeView = (PssgNodeViewModel)parameter; - AddAttributeWindow aaw = new AddAttributeWindow(); - - if (await aaw.ShowDialog() == true) + var res = await PssgDialog.ShowAddAttributeDialog(); + if (res is not null) { - PssgAttribute attr = nodeView.Node.AddAttribute(aaw.AttributeName, Convert.ChangeType(aaw.Value, aaw.AttributeValueType)); + PssgAttribute attr = nodeView.Node.AddAttribute(res.Name, Convert.ChangeType(res.Value, res.Type)); if (attr == null) { return; diff --git a/src/EgoPssgEditor/ViewModels/PssgTextureViewModel.cs b/src/EgoPssgEditor/ViewModels/PssgTextureViewModel.cs index 9166018..b2eded1 100644 --- a/src/EgoPssgEditor/ViewModels/PssgTextureViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/PssgTextureViewModel.cs @@ -1,11 +1,7 @@ using BCnEncoder.Decoder; using EgoEngineLibrary.Graphics; -using SixLabors.ImageSharp; + using SixLabors.ImageSharp.PixelFormats; -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Windows; using Avalonia; using Avalonia.Media.Imaging; diff --git a/src/EgoPssgEditor/ViewModels/TexturesWorkspaceViewModel.cs b/src/EgoPssgEditor/ViewModels/TexturesWorkspaceViewModel.cs index 0a96f7d..2907e66 100644 --- a/src/EgoPssgEditor/ViewModels/TexturesWorkspaceViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/TexturesWorkspaceViewModel.cs @@ -1,26 +1,16 @@ using EgoEngineLibrary.Graphics; using EgoEngineLibrary.Graphics.Dds; -using Microsoft.Win32; -using System; -using System.Collections.Generic; + using System.Collections.ObjectModel; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; using ActiproSoftware.UI.Avalonia.Data; -using Avalonia.Platform.Storage; - using CommunityToolkit.Mvvm.Input; -using EgoEngineLibrary.Avalonia; -using EgoEngineLibrary.Avalonia.MessageBox; +using EgoEngineLibrary.Frontend.Dialogs.File; +using EgoEngineLibrary.Frontend.Dialogs.MessageBox; -using EgoPssgEditor.Views; +using EgoPssgEditor.Dialogs.Pssg; namespace EgoPssgEditor.ViewModels { @@ -138,12 +128,12 @@ private async Task Export(object parameter) PssgNode node = ((PssgTextureViewModel)parameter).Texture; FileSaveOptions saveOptions = new() { - FileTypeChoices = [FilePickerTypes.Dds, FilePickerFileTypes.All], + FileTypeChoices = [FilePickerType.Dds, FilePickerType.All], Title = "Select the dds save location and file name", FileName = node.Attributes["id"].DisplayValue + ".dds", }; - var result = await mainView.FileSaveInteraction.HandleAsync(saveOptions); + var result = await FileDialog.ShowSaveFileDialog(saveOptions); if (result is not null) { try @@ -170,17 +160,17 @@ private async Task Import(object parameter) PssgNode node = texView.Texture; FileOpenOptions openOptions = new() { - FileTypeChoices = [FilePickerTypes.Dds, FilePickerFileTypes.All], + FileTypeChoices = [FilePickerType.Dds, FilePickerType.All], Title = "Select a dds file", FileName = node.Attributes["id"].DisplayValue + ".dds", }; - var result = await mainView.FileOpenInteraction.HandleAsync(openOptions); - if (result is not null) + var result = await FileDialog.ShowOpenFileDialog(openOptions); + if (result.Count > 0) { try { - DdsFile dds = new DdsFile(File.Open(result, FileMode.Open)); + DdsFile dds = new DdsFile(File.Open(result[0], FileMode.Open)); dds.ToPssgNode(node); texView.GetPreview(); } @@ -268,15 +258,17 @@ private bool DuplicateTexture_CanExecute(object parameter) private async Task DuplicateTexture(object parameter) { PssgTextureViewModel texView = (PssgTextureViewModel)parameter; - DuplicateTextureWindow dtw = new DuplicateTextureWindow(); - dtw.TextureName = texView.DisplayName + "_2"; + var dtVm = new DuplicateTextureViewModel + { + TextureName = texView.DisplayName + "_2" + }; - if (await dtw.ShowDialog() == true) + if (await PssgDialog.ShowDuplicateTextureDialog(dtVm)) { // Copy and Edit Name PssgNode nodeToCopy = texView.Texture; PssgNode newTexture = new PssgNode(nodeToCopy); - newTexture.Attributes["id"].Value = dtw.TextureName; + newTexture.Attributes["id"].Value = dtVm.TextureName; // Add to Library if (nodeToCopy.ParentNode != null) diff --git a/src/EgoPssgEditor/Views/MainWindow.axaml b/src/EgoPssgEditor/Views/MainWindow.axaml index f8e9278..36358e1 100644 --- a/src/EgoPssgEditor/Views/MainWindow.axaml +++ b/src/EgoPssgEditor/Views/MainWindow.axaml @@ -126,11 +126,9 @@ - + FontFamily="Consolas"> diff --git a/src/EgoPssgEditor/Views/MainWindow.axaml.cs b/src/EgoPssgEditor/Views/MainWindow.axaml.cs index 271535d..d496b5b 100644 --- a/src/EgoPssgEditor/Views/MainWindow.axaml.cs +++ b/src/EgoPssgEditor/Views/MainWindow.axaml.cs @@ -4,9 +4,11 @@ using Avalonia.Controls; using Avalonia.Interactivity; -using Avalonia.Platform.Storage; -using EgoEngineLibrary.Avalonia; +using EgoEngineLibrary.Frontend.Dialogs.File; +using EgoEngineLibrary.Frontend.Dialogs.MessageBox; + +using EgoPssgEditor.Dialogs.Pssg; namespace EgoPssgEditor.Views { @@ -28,91 +30,30 @@ namespace EgoPssgEditor.Views public partial class MainWindow : Window { private MainViewModel? view; - private IDisposable? _fileOpenInteractionRegistration; - private IDisposable? _fileSaveInteractionRegistration; public MainWindow() { InitializeComponent(); - } - - private void websiteMenuItem_Click(object sender, RoutedEventArgs e) - { - Process.Start(new ProcessStartInfo("https://petar.page/l/ego-epe-home") { UseShellExecute = true }); - } - - private void sourceCodeMenuItem_Click(object sender, RoutedEventArgs e) - { - Process.Start(new ProcessStartInfo("https://petar.page/l/ego-epe-code") { UseShellExecute = true }); + + FileDialogAvalonia.Register(this); + MessageBoxAvalonia.Register(this); + PssgDialogAvalonia.Register(this); } protected override void OnDataContextChanged(EventArgs e) { view = this.DataContext as MainViewModel; - - // Dispose any old handler - _fileOpenInteractionRegistration?.Dispose(); - _fileSaveInteractionRegistration?.Dispose(); - - if (view is not null) - { - // register the interaction handler - _fileOpenInteractionRegistration = view.FileOpenInteraction.RegisterHandler(FileOpenHandler); - _fileSaveInteractionRegistration = view.FileSaveInteraction.RegisterHandler(FileSaveHandler); - } - base.OnDataContextChanged(e); } - - private async Task FileOpenHandler(FileOpenOptions openOptions) - { - // Get a reference to our TopLevel (in our case the parent Window) - var topLevel = GetTopLevel(this); - if (topLevel is null) - { - return null; - } - - var options = new FilePickerOpenOptions - { - Title = openOptions.Title, - FileTypeFilter = openOptions.FileTypeChoices, - SuggestedFileType = openOptions.SuggestedFileType, - AllowMultiple = openOptions.AllowMultiple, - SuggestedFileName = openOptions.FileName, - SuggestedStartLocation = openOptions.InitialDirectory is null - ? null - : await topLevel.StorageProvider.TryGetFolderFromPathAsync(openOptions.InitialDirectory), - }; - var storageFiles = await topLevel.StorageProvider.OpenFilePickerAsync(options); - return storageFiles.Count <= 0 ? null : storageFiles[0].Path.LocalPath; - } - - private async Task FileSaveHandler(FileSaveOptions saveOptions) + private void websiteMenuItem_Click(object sender, RoutedEventArgs e) { - // Get a reference to our TopLevel (in our case the parent Window) - var topLevel = GetTopLevel(this); - if (topLevel is null) - { - return null; - } - - var options = new FilePickerSaveOptions - { - Title = saveOptions.Title, - FileTypeChoices = saveOptions.FileTypeChoices, - SuggestedFileType = saveOptions.SuggestedFileType, - DefaultExtension = saveOptions.DefaultExtension, - ShowOverwritePrompt = saveOptions.ShowOverwritePrompt, - SuggestedFileName = saveOptions.FileName, - SuggestedStartLocation = saveOptions.InitialDirectory is null - ? null - : await topLevel.StorageProvider.TryGetFolderFromPathAsync(saveOptions.InitialDirectory), - }; + Process.Start(new ProcessStartInfo("https://petar.page/l/ego-epe-home") { UseShellExecute = true }); + } - var storageFiles = await topLevel.StorageProvider.SaveFilePickerAsync(options); - return storageFiles?.Path.LocalPath; + private void sourceCodeMenuItem_Click(object sender, RoutedEventArgs e) + { + Process.Start(new ProcessStartInfo("https://petar.page/l/ego-epe-code") { UseShellExecute = true }); } } } From 63a8e2084a010c5b41b5dd222ef0e95daece8356 Mon Sep 17 00:00:00 2001 From: Petar Tasev Date: Sat, 31 Jan 2026 21:39:08 -0800 Subject: [PATCH 4/4] UI adjustments and other minor changes --- Directory.Packages.props | 21 ++++++++++--------- src/EgoPssgEditor/App.axaml | 4 ++++ src/EgoPssgEditor/ViewModels/MainViewModel.cs | 2 +- .../ViewModels/PssgTextureViewModel.cs | 4 ++++ .../ViewModels/TexturesWorkspaceViewModel.cs | 6 +----- .../Views/AddAttributeWindow.axaml | 6 +++--- src/EgoPssgEditor/Views/AddNodeWindow.axaml | 6 +++--- .../Views/DuplicateTextureWindow.axaml | 4 ++-- src/EgoPssgEditor/Views/MainWindow.axaml | 5 ++++- 9 files changed, 33 insertions(+), 25 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index defcb1b..3295972 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,29 +2,30 @@ true false + 11.3.11 - + - - - - - - + + + + + + - + - - + + diff --git a/src/EgoPssgEditor/App.axaml b/src/EgoPssgEditor/App.axaml index fb04444..bcafe07 100644 --- a/src/EgoPssgEditor/App.axaml +++ b/src/EgoPssgEditor/App.axaml @@ -10,5 +10,9 @@ + + diff --git a/src/EgoPssgEditor/ViewModels/MainViewModel.cs b/src/EgoPssgEditor/ViewModels/MainViewModel.cs index 9a1af46..fb2aeb0 100644 --- a/src/EgoPssgEditor/ViewModels/MainViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/MainViewModel.cs @@ -68,7 +68,7 @@ public int SelectedTabIndex public MainViewModel() { this.DisplayName = Properties.Resources.AppTitleLong; - schemaPath = AppDomain.CurrentDomain.BaseDirectory + "\\schema.xml"; + schemaPath = Path.Combine(AppContext.BaseDirectory, "schema.xml"); nodesWorkspace = new NodesWorkspaceViewModel(this); texturesWorkspace = new TexturesWorkspaceViewModel(this); diff --git a/src/EgoPssgEditor/ViewModels/PssgTextureViewModel.cs b/src/EgoPssgEditor/ViewModels/PssgTextureViewModel.cs index b2eded1..1183a09 100644 --- a/src/EgoPssgEditor/ViewModels/PssgTextureViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/PssgTextureViewModel.cs @@ -58,7 +58,9 @@ public bool IsSelected } else { + Preview?.Dispose(); Preview = null; + NodeView.IsSelected = false; } OnPropertyChanged(nameof(IsSelected)); } @@ -91,6 +93,7 @@ public void GetPreview() var ddsReadSuccess = false; try { + Preview?.Dispose(); Preview = null; BCnEncoder.Shared.ImageFiles.DdsFile bcDds; using (var ms = new MemoryStream()) @@ -144,6 +147,7 @@ public void GetPreview() } catch (Exception ex) { + Preview?.Dispose(); Preview = null; if (ddsReadSuccess) PreviewError = "Could not create preview! Export/Import may still work." + Environment.NewLine + Environment.NewLine + ex.Message; diff --git a/src/EgoPssgEditor/ViewModels/TexturesWorkspaceViewModel.cs b/src/EgoPssgEditor/ViewModels/TexturesWorkspaceViewModel.cs index 2907e66..49e76e1 100644 --- a/src/EgoPssgEditor/ViewModels/TexturesWorkspaceViewModel.cs +++ b/src/EgoPssgEditor/ViewModels/TexturesWorkspaceViewModel.cs @@ -63,10 +63,6 @@ public override void LoadData(object data) { ClearData(); LoadTextures((PssgNodeViewModel)data); - //foreach (PssgNode texture in file.RootNode.FindNodes("TEXTURE", "id")) - //{ - // textures.Add(new PssgTextureViewModel(texture)); - //} } public void LoadTextures(PssgNodeViewModel nodeView) { @@ -197,7 +193,7 @@ private async Task ExportTextures() foreach (PssgTextureViewModel texView in Textures) { dds = texView.Texture.ToDdsFile(false); - string filePath = texDir + "\\" + texView.Texture.Attributes["id"].DisplayValue + ".dds"; + string filePath = Path.Combine(texDir, texView.Texture.Attributes["id"].DisplayValue + ".dds"); using (var fs = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) dds.Write(fs, -1); } diff --git a/src/EgoPssgEditor/Views/AddAttributeWindow.axaml b/src/EgoPssgEditor/Views/AddAttributeWindow.axaml index b6dd4fe..b601366 100644 --- a/src/EgoPssgEditor/Views/AddAttributeWindow.axaml +++ b/src/EgoPssgEditor/Views/AddAttributeWindow.axaml @@ -7,8 +7,8 @@ mc:Ignorable="d" Title="Add Attribute" Height="210" Width="300" CanResize="False" CanMinimize="False"> - - + + @@ -27,7 +27,7 @@ - + diff --git a/src/EgoPssgEditor/Views/AddNodeWindow.axaml b/src/EgoPssgEditor/Views/AddNodeWindow.axaml index cbc93eb..aee4867 100644 --- a/src/EgoPssgEditor/Views/AddNodeWindow.axaml +++ b/src/EgoPssgEditor/Views/AddNodeWindow.axaml @@ -6,8 +6,8 @@ mc:Ignorable="d" Title="Add Node" Height="150" Width="300" CanResize="False" CanMinimize="False"> - - + + @@ -22,7 +22,7 @@ - + diff --git a/src/EgoPssgEditor/Views/DuplicateTextureWindow.axaml b/src/EgoPssgEditor/Views/DuplicateTextureWindow.axaml index 6beffd4..5079dfa 100644 --- a/src/EgoPssgEditor/Views/DuplicateTextureWindow.axaml +++ b/src/EgoPssgEditor/Views/DuplicateTextureWindow.axaml @@ -6,8 +6,8 @@ mc:Ignorable="d" Title="Duplicate Texture" Height="150" Width="300" CanResize="False" CanMinimize="False"> - - + + diff --git a/src/EgoPssgEditor/Views/MainWindow.axaml b/src/EgoPssgEditor/Views/MainWindow.axaml index 36358e1..622c534 100644 --- a/src/EgoPssgEditor/Views/MainWindow.axaml +++ b/src/EgoPssgEditor/Views/MainWindow.axaml @@ -113,7 +113,10 @@ - +