diff --git a/PKHeX.Avalonia/App.axaml.cs b/PKHeX.Avalonia/App.axaml.cs index c284776f7..cdac788b4 100644 --- a/PKHeX.Avalonia/App.axaml.cs +++ b/PKHeX.Avalonia/App.axaml.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; @@ -34,10 +35,30 @@ public override void OnFrameworkInitializationCompleted() if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { var mainViewModel = new MainWindowViewModel(); - desktop.MainWindow = new MainWindow + var mainWindow = new MainWindow { DataContext = mainViewModel, }; + desktop.MainWindow = mainWindow; + + // Auto-load save file on startup, after the window is shown + mainWindow.Opened += (_, _) => + { + try + { + var args = Environment.GetCommandLineArgs().Skip(1).ToArray(); + var startup = StartupUtil.GetStartup(args, Settings); + if (startup.SAV is { } sav) + { + var path = sav.Metadata.FilePath ?? string.Empty; + mainViewModel.LoadInitialSave(sav, startup.Entity, path); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Auto-load on startup failed: {ex.Message}"); + } + }; desktop.ShutdownRequested += (_, _) => { @@ -61,6 +82,11 @@ private void LoadSettings() Settings.LocalResources.SetLocalPath(WorkingDirectory); SpriteBuilder.LoadSettings(Settings.Sprite); + // Handle HaX and version tracking (mirrors WinForms FormLoadInitialActions) + var args = Environment.GetCommandLineArgs().Skip(1).ToArray(); + var init = StartupUtil.FormLoadInitialActions(args, Settings, CurrentVersion); + HaX = init.HaX; + var language = Settings.Startup.Language; LocalizeUtil.InitializeStrings(language); } diff --git a/PKHeX.Avalonia/Controls/SlotChangeManager.cs b/PKHeX.Avalonia/Controls/SlotChangeManager.cs index 5a0224e15..56b2c7fe2 100644 --- a/PKHeX.Avalonia/Controls/SlotChangeManager.cs +++ b/PKHeX.Avalonia/Controls/SlotChangeManager.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using Avalonia.Input; using Avalonia.Platform.Storage; using PKHeX.Avalonia.ViewModels; +using PKHeX.Avalonia.ViewModels.Subforms; using PKHeX.Core; namespace PKHeX.Avalonia.Controls; @@ -23,17 +25,33 @@ public sealed class SlotChangeManager private readonly SAVEditorViewModel _editor; + /// Registered BoxViewer VMs whose slots participate in drag-and-drop. + private readonly List _boxViewers = []; + /// Tracks the source slot when a drag operation is in progress. private SlotModel? _sourceSlot; /// Indicates whether a drag operation is currently in progress. public bool IsDragInProgress { get; private set; } + /// Gets the that owns this manager. + public SAVEditorViewModel Editor => _editor; + public SlotChangeManager(SAVEditorViewModel editor) { _editor = editor; } + /// + /// Registers a so its slots can participate in drag-and-drop. + /// + public void RegisterBoxViewer(BoxViewerViewModel viewer) => _boxViewers.Add(viewer); + + /// + /// Unregisters a when its window is closed. + /// + public void UnregisterBoxViewer(BoxViewerViewModel viewer) => _boxViewers.Remove(viewer); + /// /// Initiates a drag operation from the given slot. /// Call this from move after a press. @@ -46,7 +64,7 @@ public SlotChangeManager(SAVEditorViewModel editor) if (IsDragInProgress) return null; - var pk = _editor.GetSlotPKM(slot); + var pk = GetSlotPKM(slot); if (pk is null || pk.Species == 0) return null; @@ -156,6 +174,12 @@ private void HandleFileDrop(SlotModel destSlot, DragEventArgs e) _editor.PushUndo(undoEntry); WriteSlot(destSlot, pk); _editor.ReloadSlots(); + // Refresh box viewer if the drop target was in one + foreach (var bv in _boxViewers) + { + if (bv.BoxSlots.Contains(destSlot)) + bv.RefreshBox(); + } _editor.SetStatusMessage?.Invoke($"Loaded {Path.GetFileName(filePath)} into slot."); } catch (Exception ex) @@ -179,6 +203,57 @@ private static DropModifier GetDropModifier(KeyModifiers keys) return DropModifier.None; } + /// + /// Resolved location of a within the save file. + /// + private readonly record struct ResolvedSlot(int Box, int Index, bool IsParty, BoxViewerViewModel? BoxViewer); + + /// + /// Resolves a to its (box, index) coordinate by checking + /// the main editor's box/party slots and all registered box viewers. + /// + private ResolvedSlot? ResolveSlot(SlotModel slot) + { + // Check main editor box slots + int boxIndex = _editor.BoxSlots.IndexOf(slot); + if (boxIndex >= 0) + return new ResolvedSlot(_editor.CurrentBox, boxIndex, false, null); + + // Check main editor party slots + int partyIndex = _editor.PartySlots.IndexOf(slot); + if (partyIndex >= 0) + return new ResolvedSlot(0, partyIndex, true, null); + + // Check registered box viewers + foreach (var bv in _boxViewers) + { + int bvIndex = bv.BoxSlots.IndexOf(slot); + if (bvIndex >= 0) + return new ResolvedSlot(bv.CurrentBox, bvIndex, false, bv); + } + + return null; + } + + /// + /// Gets the PKM at the resolved slot location. + /// + private PKM? GetSlotPKM(SlotModel slot) + { + var sav = _editor.SAV; + if (sav is null) + return null; + + var resolved = ResolveSlot(slot); + if (resolved is null) + return null; + + var r = resolved.Value; + if (r.IsParty) + return sav.GetPartySlotAtIndex(r.Index); + return sav.GetBoxSlotAtIndex(r.Box, r.Index); + } + /// /// Executes the slot move/swap/clone operation. /// @@ -188,15 +263,15 @@ private void PerformSlotOperation(SlotModel source, SlotModel dest, DropModifier if (sav is null) return; - var sourcePkm = _editor.GetSlotPKM(source); + var sourcePkm = GetSlotPKM(source); if (sourcePkm is null || sourcePkm.Species == 0) return; - var destPkm = _editor.GetSlotPKM(dest); + var destPkm = GetSlotPKM(dest); bool destIsEmpty = destPkm is null || destPkm.Species == 0; // Collect undo entries and push as a single atomic group - var entries = new System.Collections.Generic.List(); + var entries = new List(); var destEntry = CreateUndoEntry(dest); if (destEntry is not null) entries.Add(destEntry); @@ -222,8 +297,9 @@ private void PerformSlotOperation(SlotModel source, SlotModel dest, DropModifier case DropModifier.Overwrite: // Don't clear source if it's the last party member { - int sourcePartyIdx = _editor.PartySlots.IndexOf(source); - if (sourcePartyIdx < 0 || sav.PartyCount > 1) + var sourceResolved = ResolveSlot(source); + bool isParty = sourceResolved is { IsParty: true }; + if (!isParty || sav.PartyCount > 1) ClearSlot(source); } break; @@ -232,8 +308,9 @@ private void PerformSlotOperation(SlotModel source, SlotModel dest, DropModifier if (destIsEmpty) { // Don't clear source if it's the last party member - int sourcePartyIdx = _editor.PartySlots.IndexOf(source); - if (sourcePartyIdx < 0 || sav.PartyCount > 1) + var sourceResolved = ResolveSlot(source); + bool isParty = sourceResolved is { IsParty: true }; + if (!isParty || sav.PartyCount > 1) ClearSlot(source); } else @@ -245,6 +322,20 @@ private void PerformSlotOperation(SlotModel source, SlotModel dest, DropModifier } _editor.ReloadSlots(); + // Refresh any box viewers that were involved + RefreshBoxViewers(source, dest); + } + + /// + /// Refreshes box viewers whose slots were involved in a drag-drop operation. + /// + private void RefreshBoxViewers(SlotModel source, SlotModel dest) + { + foreach (var bv in _boxViewers) + { + if (bv.BoxSlots.Contains(source) || bv.BoxSlots.Contains(dest)) + bv.RefreshBox(); + } } /// @@ -257,23 +348,23 @@ private void PerformSlotOperation(SlotModel source, SlotModel dest, DropModifier if (sav is null) return null; - int boxIndex = _editor.BoxSlots.IndexOf(slot); - if (boxIndex >= 0) - { - var existing = sav.GetBoxSlotAtIndex(_editor.CurrentBox, boxIndex); - if (existing is null) return null; - return new SAVEditorViewModel.SlotChangeEntry(_editor.CurrentBox, boxIndex, existing.DecryptedBoxData, false); - } + var resolved = ResolveSlot(slot); + if (resolved is null) + return null; - int partyIndex = _editor.PartySlots.IndexOf(slot); - if (partyIndex >= 0) + var r = resolved.Value; + if (r.IsParty) { - var existing = sav.GetPartySlotAtIndex(partyIndex); + var existing = sav.GetPartySlotAtIndex(r.Index); if (existing is null) return null; - return new SAVEditorViewModel.SlotChangeEntry(0, partyIndex, existing.DecryptedBoxData, true); + return new SAVEditorViewModel.SlotChangeEntry(0, r.Index, existing.DecryptedBoxData, true); + } + else + { + var existing = sav.GetBoxSlotAtIndex(r.Box, r.Index); + if (existing is null) return null; + return new SAVEditorViewModel.SlotChangeEntry(r.Box, r.Index, existing.DecryptedBoxData, false); } - - return null; } /// @@ -285,18 +376,15 @@ private void WriteSlot(SlotModel slot, PKM pk) if (sav is null) return; - int boxIndex = _editor.BoxSlots.IndexOf(slot); - if (boxIndex >= 0) - { - sav.SetBoxSlotAtIndex(pk, _editor.CurrentBox, boxIndex); + var resolved = ResolveSlot(slot); + if (resolved is null) return; - } - int partyIndex = _editor.PartySlots.IndexOf(slot); - if (partyIndex >= 0) - { - sav.SetPartySlotAtIndex(pk, partyIndex); - } + var r = resolved.Value; + if (r.IsParty) + sav.SetPartySlotAtIndex(pk, r.Index); + else + sav.SetBoxSlotAtIndex(pk, r.Box, r.Index); } /// @@ -308,19 +396,20 @@ private void ClearSlot(SlotModel slot) if (sav is null) return; - int boxIndex = _editor.BoxSlots.IndexOf(slot); - if (boxIndex >= 0) - { - sav.SetBoxSlotAtIndex(sav.BlankPKM, _editor.CurrentBox, boxIndex); + var resolved = ResolveSlot(slot); + if (resolved is null) return; - } - int partyIndex = _editor.PartySlots.IndexOf(slot); - if (partyIndex >= 0) + var r = resolved.Value; + if (r.IsParty) { if (sav.PartyCount <= 1) return; - sav.DeletePartySlot(partyIndex); + sav.DeletePartySlot(r.Index); + } + else + { + sav.SetBoxSlotAtIndex(sav.BlankPKM, r.Box, r.Index); } } } diff --git a/PKHeX.Avalonia/ViewModels/MainWindowViewModel.cs b/PKHeX.Avalonia/ViewModels/MainWindowViewModel.cs index 30ff66f4a..30833a11b 100644 --- a/PKHeX.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/MainWindowViewModel.cs @@ -135,6 +135,11 @@ private void RefreshGameDataAfterLanguageChange() /// private PluginLoadResult? _pluginLoadResult; + /// + /// Plugin host that provides ISaveFileProvider to plugins. + /// + private AvaloniaPluginHost? _pluginHost; + public MainWindowViewModel() : this(new AvaloniaDialogService()) { } @@ -168,15 +173,27 @@ public MainWindowViewModel(IDialogService dialogService) /// private void LoadPlugins() { + if (!App.Settings.Startup.PluginLoadEnable) + return; + var pluginPath = Path.Combine(App.WorkingDirectory, "plugins"); try { - _pluginLoadResult = PluginLoader.LoadPlugins(pluginPath, Plugins, false); + _pluginLoadResult = PluginLoader.LoadPlugins(pluginPath, Plugins, App.Settings.Startup.PluginLoadMerged); + + // Create the plugin host that provides ISaveFileProvider to plugins + var blankSav = BlankSaveFile.Get(App.Settings.Startup.DefaultSaveVersion, null); + _pluginHost = new AvaloniaPluginHost( + blankSav, + () => SavEditor?.CurrentBox ?? 0, + () => SavEditor?.ReloadSlots() + ); + foreach (var plugin in Plugins.OrderBy(p => p.Priority)) { try { - plugin.Initialize(this); + plugin.Initialize(_pluginHost, App.CurrentVersion); } catch (Exception ex) { @@ -353,6 +370,17 @@ private async Task DumpAllBoxesAsync() } } + /// + /// Loads the initial save file and entity from startup arguments. + /// Called by App.axaml.cs after the main window is shown. + /// + public void LoadInitialSave(SaveFile sav, PKM? entity, string path) + { + LoadSaveFile(sav, path); + if (entity is not null) + PkmEditor?.PopulateFields(entity); + } + public async Task LoadFileAsync(string path) { if (_isLoading) @@ -411,6 +439,9 @@ private void LoadSaveFile(SaveFile sav, string path) SavEditor?.LoadSaveFile(sav); PkmEditor?.Initialize(sav); + // Update the plugin host with the new save file + _pluginHost?.UpdateSaveFile(sav); + App.Settings.Startup.LoadSaveFile(path); Title = $"PKHeX - {sav.GetType().Name} ({Path.GetFileName(path)})"; @@ -574,10 +605,14 @@ private void OpenBoxViewer() try { + var slotManager = SavEditor?.SlotManager; var vm = new BoxViewerViewModel(SaveFile, SavEditor?.CurrentBox ?? 0) { SlotSelected = pk => PkmEditor?.PopulateFields(pk), + SlotManager = slotManager, }; + slotManager?.RegisterBoxViewer(vm); + var view = new BoxViewerView { DataContext = vm }; var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow; @@ -585,7 +620,11 @@ private void OpenBoxViewer() { view.Show(mainWindow); // non-modal _openSubWindows.Add(view); - view.Closed += (_, _) => _openSubWindows.Remove(view); + view.Closed += (_, _) => + { + _openSubWindows.Remove(view); + slotManager?.UnregisterBoxViewer(vm); + }; } } catch (Exception ex) diff --git a/PKHeX.Avalonia/ViewModels/Subforms/BoxViewerViewModel.cs b/PKHeX.Avalonia/ViewModels/Subforms/BoxViewerViewModel.cs index f6c3a41a0..e62726bbf 100644 --- a/PKHeX.Avalonia/ViewModels/Subforms/BoxViewerViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/Subforms/BoxViewerViewModel.cs @@ -30,6 +30,12 @@ public partial class BoxViewerViewModel : ObservableObject /// Callback invoked when a slot is clicked to view its PKM. public Action? SlotSelected { get; set; } + /// + /// Shared from the main editor, enabling drag-and-drop + /// between the BoxViewer and the main SAV editor. + /// + public SlotChangeManager? SlotManager { get; set; } + public BoxViewerViewModel(SaveFile sav, int initialBox = 0) { _sav = sav; diff --git a/PKHeX.Avalonia/ViewModels/Subforms/FolderListViewModel.cs b/PKHeX.Avalonia/ViewModels/Subforms/FolderListViewModel.cs index ed735bb75..4f5290052 100644 --- a/PKHeX.Avalonia/ViewModels/Subforms/FolderListViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/Subforms/FolderListViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; @@ -73,10 +74,22 @@ public partial class FolderListViewModel : ObservableObject public string WindowTitle => "Save File Folder List"; + /// + /// Callback invoked when the user requests to open a file from the Recent or Backup list. + /// The caller (MainWindowViewModel) should subscribe to this to load the selected file. + /// + public Action? FileOpenRequested { get; set; } + + /// + /// Set to true when a file has been opened (so the dialog can close). + /// + [ObservableProperty] + private bool _fileOpened; + /// /// Creates the view model with the given folder paths and backup directory. /// - public FolderListViewModel(string[] folderPaths, string backupPath) + public FolderListViewModel(string[] folderPaths, string backupPath, IReadOnlyList? recentlyLoaded = null) { foreach (var path in folderPaths) { @@ -84,18 +97,31 @@ public FolderListViewModel(string[] folderPaths, string backupPath) Folders.Add(new FolderEntry(Path.GetFileName(path), path)); } - // Load recent files from common save locations - foreach (var folder in Folders) + // Load recent files from the settings RecentlyLoaded list (matching WinForms behavior) + if (recentlyLoaded is { Count: > 0 }) { - if (!Directory.Exists(folder.FullPath)) - continue; - try + foreach (var recentPath in recentlyLoaded) { - foreach (var file in Directory.EnumerateFiles(folder.FullPath, "*", SearchOption.TopDirectoryOnly).Take(50)) - RecentFiles.Add(new SaveFileEntry(file)); + if (File.Exists(recentPath)) + RecentFiles.Add(new SaveFileEntry(recentPath)); + } + } + + // Also include files from folder paths if no recently loaded entries + if (RecentFiles.Count == 0) + { + foreach (var folder in Folders) + { + if (!Directory.Exists(folder.FullPath)) + continue; + try + { + foreach (var file in Directory.EnumerateFiles(folder.FullPath, "*", SearchOption.TopDirectoryOnly).Take(50)) + RecentFiles.Add(new SaveFileEntry(file)); + } + catch (UnauthorizedAccessException) { } + catch (IOException) { } } - catch (UnauthorizedAccessException) { } - catch (IOException) { } } // Load backup files @@ -155,4 +181,18 @@ private void OpenFolder(FolderEntry? folder) } catch { /* ignore failures to open folder */ } } + + /// + /// Opens the selected save file entry by invoking the callback. + /// Triggered by double-clicking a row in the Recent or Backup DataGrid. + /// + [RelayCommand] + private void OpenSaveFile(SaveFileEntry? entry) + { + if (entry is null || !File.Exists(entry.FilePath)) + return; + + FileOpenRequested?.Invoke(entry.FilePath); + FileOpened = true; + } } diff --git a/PKHeX.Avalonia/ViewModels/Subforms/Misc4ViewModel.cs b/PKHeX.Avalonia/ViewModels/Subforms/Misc4ViewModel.cs index adb68c8bb..41c6bd713 100644 --- a/PKHeX.Avalonia/ViewModels/Subforms/Misc4ViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/Subforms/Misc4ViewModel.cs @@ -69,6 +69,7 @@ public WalkerCourseModel(int index, string name, bool unlocked) /// public partial class Misc4ViewModel : SaveEditorViewModelBase { + private readonly SAV4 _origin; private readonly SAV4 SAV4; // General @@ -130,35 +131,36 @@ public partial class Misc4ViewModel : SaveEditorViewModelBase public Misc4ViewModel(SAV4 sav) : base(sav) { - SAV4 = sav; - Record = sav.Records; + _origin = sav; + SAV4 = (SAV4)sav.Clone(); + Record = SAV4.Records; - _maxCoins = (uint)sav.MaxCoins; - _coins = Math.Clamp(sav.Coin, 0, _maxCoins); - _bp = Math.Clamp(sav.BP, 0, 9999); + _maxCoins = (uint)SAV4.MaxCoins; + _coins = Math.Clamp(SAV4.Coin, 0, _maxCoins); + _bp = Math.Clamp(SAV4.BP, 0, 9999); PoketchAppNames = GameInfo.Strings.poketchapps; // Fly destinations - var locations = sav is SAV4Sinnoh ? LocationIDsSinnoh : LocationIDsHGSS; - var flags = sav is SAV4Sinnoh ? FlyWorkFlagSinnoh : FlyWorkFlagHGSS; + var locations = SAV4 is SAV4Sinnoh ? LocationIDsSinnoh : LocationIDsHGSS; + var flags = SAV4 is SAV4Sinnoh ? FlyWorkFlagSinnoh : FlyWorkFlagHGSS; for (int i = 0; i < locations.Length; i++) { var flagIndex = FlyFlagStart + flags[i]; - var state = sav.GetEventFlag(flagIndex); + var state = SAV4.GetEventFlag(flagIndex); var locationID = locations[i]; var name = GameInfo.Strings.Gen4.Met0[locationID]; FlyDestinations.Add(new FlyDestModel(flagIndex, name, state)); } // Sinnoh-specific - ShowPoketch = sav is SAV4Sinnoh; - ShowUGFlags = sav is SAV4Sinnoh; - ShowWalker = sav is SAV4HGSS; - ShowPokeathlon = sav is SAV4HGSS; - ShowMap = sav is SAV4HGSS; + ShowPoketch = SAV4 is SAV4Sinnoh; + ShowUGFlags = SAV4 is SAV4Sinnoh; + ShowWalker = SAV4 is SAV4HGSS; + ShowPokeathlon = SAV4 is SAV4HGSS; + ShowMap = SAV4 is SAV4HGSS; - if (sav is SAV4Sinnoh sinnoh) + if (SAV4 is SAV4Sinnoh sinnoh) { _ugFlagsCaptured = Math.Clamp(sinnoh.UG_FlagsCaptured, 0, SAV4Sinnoh.UG_MAX); @@ -170,7 +172,7 @@ public Misc4ViewModel(SAV4 sav) : base(sav) } _currentPoketchApp = sinnoh.CurrentPoketchApp; } - else if (sav is SAV4HGSS hgss) + else if (SAV4 is SAV4HGSS hgss) { // Walker ReadOnlySpan walkerCourseNames = GameInfo.Sources.Strings.walkercourses; @@ -275,7 +277,7 @@ private void Save() Record.SetRecord32(Record32Index, Record32Value); Record.EndAccess(); - SAV.State.Edited = true; + _origin.CopyChangesFrom(SAV4); Modified = true; } } diff --git a/PKHeX.Avalonia/ViewModels/Subforms/Misc5ViewModel.cs b/PKHeX.Avalonia/ViewModels/Subforms/Misc5ViewModel.cs index 9532748a1..62a3fd7ae 100644 --- a/PKHeX.Avalonia/ViewModels/Subforms/Misc5ViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/Subforms/Misc5ViewModel.cs @@ -72,6 +72,7 @@ public PropModel(int index, string name, bool obtained) /// public partial class Misc5ViewModel : SaveEditorViewModelBase { + private readonly SAV5 _origin; private readonly SAV5 SAV5; private readonly BattleSubway5 Subway; private readonly BattleSubwayPlay5 SubwayPlay; @@ -141,10 +142,11 @@ public partial class Misc5ViewModel : SaveEditorViewModelBase public Misc5ViewModel(SAV5 sav) : base(sav) { - SAV5 = sav; - Subway = sav.BattleSubway; - SubwayPlay = sav.BattleSubwayPlay; - Record = sav.Records; + _origin = sav; + SAV5 = (SAV5)sav.Clone(); + Subway = SAV5.BattleSubway; + SubwayPlay = SAV5.BattleSubwayPlay; + Record = SAV5.Records; ReadFly(); ReadRoamer(); @@ -153,8 +155,8 @@ public Misc5ViewModel(SAV5 sav) : base(sav) ReadSubway(); ReadRecords(); - ShowRoamer = sav is SAV5BW; - ShowKeySystem = sav is SAV5B2W2; + ShowRoamer = SAV5 is SAV5BW; + ShowKeySystem = SAV5 is SAV5B2W2; } private void ReadFly() @@ -321,7 +323,7 @@ private void Save() SaveSubway(); SaveRecords(); - SAV.State.Edited = true; + _origin.CopyChangesFrom(SAV5); Modified = true; } diff --git a/PKHeX.Avalonia/ViewModels/Subforms/SAVSuperTrain6ViewModel.cs b/PKHeX.Avalonia/ViewModels/Subforms/SAVSuperTrain6ViewModel.cs index 5fa38892f..14e062897 100644 --- a/PKHeX.Avalonia/ViewModels/Subforms/SAVSuperTrain6ViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/Subforms/SAVSuperTrain6ViewModel.cs @@ -99,11 +99,12 @@ public SAVSuperTrain6ViewModel(SAV6 sav) : base(sav) SelectedStageIndex = 0; } - partial void OnSelectedStageIndexChanged(int value) + partial void OnSelectedStageIndexChanged(int oldValue, int newValue) { - if (value < 0) - return; - LoadStageRecord(value); + if (oldValue >= 0) + SaveStageRecord(oldValue); + if (newValue >= 0) + LoadStageRecord(newValue); } private void LoadStageRecord(int index) diff --git a/PKHeX.Avalonia/ViewModels/Subforms/SimpleTrainerViewModel.cs b/PKHeX.Avalonia/ViewModels/Subforms/SimpleTrainerViewModel.cs index 081837038..4d1e0cb35 100644 --- a/PKHeX.Avalonia/ViewModels/Subforms/SimpleTrainerViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/Subforms/SimpleTrainerViewModel.cs @@ -11,6 +11,7 @@ namespace PKHeX.Avalonia.ViewModels.Subforms; /// public partial class SimpleTrainerViewModel : SaveEditorViewModelBase { + private readonly SaveFile _origin; private readonly SaveFile _clone; [ObservableProperty] @@ -69,7 +70,8 @@ public partial class SimpleTrainerViewModel : SaveEditorViewModelBase public SimpleTrainerViewModel(SaveFile sav) : base(sav) { - _clone = sav; + _origin = sav; + _clone = (SaveFile)sav.Clone(); MaxNameLength = sav.MaxStringLengthTrainer; MaxMoney = (uint)sav.MaxMoney; @@ -142,6 +144,7 @@ private void Save() if (Badge8) badgeval |= 1 << 7; SetBadgeValue(sav, badgeval); + _origin.CopyChangesFrom(_clone); Modified = true; } diff --git a/PKHeX.Avalonia/Views/SlotControl.axaml.cs b/PKHeX.Avalonia/Views/SlotControl.axaml.cs index 4b3efbee0..18ec9d415 100644 --- a/PKHeX.Avalonia/Views/SlotControl.axaml.cs +++ b/PKHeX.Avalonia/Views/SlotControl.axaml.cs @@ -7,6 +7,7 @@ using Avalonia.VisualTree; using PKHeX.Avalonia.Controls; using PKHeX.Avalonia.ViewModels; +using PKHeX.Avalonia.ViewModels.Subforms; namespace PKHeX.Avalonia.Views; @@ -168,18 +169,36 @@ private void OnDrop(object? sender, DragEventArgs e) /// /// Walks up the visual tree to find the . + /// Falls back to reaching it through + /// when the slot lives inside a BoxViewer window. /// private SAVEditorViewModel? FindSAVEditorViewModel() { var itemsControl = this.FindAncestorOfType(); - return itemsControl?.DataContext as SAVEditorViewModel; + if (itemsControl?.DataContext is SAVEditorViewModel savVm) + return savVm; + + // BoxViewer path: reach the main editor through the shared SlotChangeManager + if (itemsControl?.DataContext is BoxViewerViewModel boxVm) + return boxVm.SlotManager?.Editor; + + return null; } /// - /// Finds the from the . + /// Finds the from either the + /// or a ancestor. /// private SlotChangeManager? FindSlotChangeManager() { - return FindSAVEditorViewModel()?.SlotManager; + var itemsControl = this.FindAncestorOfType(); + + if (itemsControl?.DataContext is SAVEditorViewModel savVm) + return savVm.SlotManager; + + if (itemsControl?.DataContext is BoxViewerViewModel boxVm) + return boxVm.SlotManager; + + return null; } } diff --git a/PKHeX.Avalonia/Views/Subforms/FolderListView.axaml b/PKHeX.Avalonia/Views/Subforms/FolderListView.axaml index 18f9e2081..4e2501fc7 100644 --- a/PKHeX.Avalonia/Views/Subforms/FolderListView.axaml +++ b/PKHeX.Avalonia/Views/Subforms/FolderListView.axaml @@ -47,11 +47,13 @@ - + GridLinesVisibility="Horizontal" + DoubleTapped="OnDataGridDoubleTapped"> @@ -60,11 +62,13 @@ - + GridLinesVisibility="Horizontal" + DoubleTapped="OnDataGridDoubleTapped"> diff --git a/PKHeX.Avalonia/Views/Subforms/FolderListView.axaml.cs b/PKHeX.Avalonia/Views/Subforms/FolderListView.axaml.cs index 70cfabdb4..22da7c570 100644 --- a/PKHeX.Avalonia/Views/Subforms/FolderListView.axaml.cs +++ b/PKHeX.Avalonia/Views/Subforms/FolderListView.axaml.cs @@ -1,4 +1,7 @@ +using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Interactivity; +using PKHeX.Avalonia.ViewModels.Subforms; namespace PKHeX.Avalonia.Views.Subforms; @@ -13,4 +16,20 @@ private void OnCancelClick(object? sender, RoutedEventArgs e) { CloseWithResult(false); } + + private void OnDataGridDoubleTapped(object? sender, TappedEventArgs e) + { + if (sender is not DataGrid grid) + return; + if (grid.SelectedItem is not SaveFileEntry entry) + return; + if (DataContext is not FolderListViewModel vm) + return; + + vm.OpenSaveFileCommand.Execute(entry); + + // Close the dialog after the file is opened + if (vm.FileOpened) + CloseWithResult(true); + } } diff --git a/PKHeX.Avalonia/Views/Subforms/MemoryAmieView.axaml b/PKHeX.Avalonia/Views/Subforms/MemoryAmieView.axaml index 61c0023ef..59e6b132c 100644 --- a/PKHeX.Avalonia/Views/Subforms/MemoryAmieView.axaml +++ b/PKHeX.Avalonia/Views/Subforms/MemoryAmieView.axaml @@ -79,7 +79,7 @@ VerticalAlignment="Center" IsVisible="{Binding HasSociability}" FontWeight="SemiBold" FontSize="11" />