Fix WinForms parity: BoxViewer DnD, auto-load, recent files, plugins

BoxViewer drag-drop (4 files):
- SlotControl: FindSlotChangeManager now resolves from BoxViewerVM
- SlotChangeManager: register/unregister box viewers, ResolveSlot
  checks main editor + all registered box viewers for slot lookups
- BoxViewerVM: accepts shared SlotChangeManager reference
- MainWindowVM: pass SlotChangeManager to BoxViewer on open

Auto-load save on startup:
- App: call StartupUtil.GetStartup() with CLI args on window open
- App: call StartupUtil.FormLoadInitialActions() for HaX/version
- MainWindowVM: add LoadInitialSave() for startup entity loading

Recent files in FolderList:
- FolderListVM: populate Recent tab from RecentlyLoaded settings
- FolderListVM: add FileOpenRequested callback + OpenSaveFile command
- FolderListView: double-tap DataGrid row opens the save file

Plugin initialization:
- MainWindowVM: check PluginLoadEnable before loading plugins
- MainWindowVM: pass AvaloniaPluginHost (ISaveFileProvider) to plugins
- MainWindowVM: update plugin host save reference on file load

Clone pattern fixes:
- SimpleTrainerVM: actually clone SAV (was storing live reference)
- Misc4VM: clone SAV4, CopyChangesFrom on save
- Misc5VM: clone SAV5, CopyChangesFrom on save
- SAVSuperTrain6VM: flush stage record before switching stages

Other:
- MemoryAmieView: Sociability max changed from 255 to 4294967295
This commit is contained in:
montanon 2026-03-17 14:45:09 -03:00
parent 970cbe6ce3
commit 875f7904ea
13 changed files with 339 additions and 89 deletions

View File

@ -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);
}

View File

@ -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;
/// <summary>Registered BoxViewer VMs whose slots participate in drag-and-drop.</summary>
private readonly List<BoxViewerViewModel> _boxViewers = [];
/// <summary>Tracks the source slot when a drag operation is in progress.</summary>
private SlotModel? _sourceSlot;
/// <summary>Indicates whether a drag operation is currently in progress.</summary>
public bool IsDragInProgress { get; private set; }
/// <summary>Gets the <see cref="SAVEditorViewModel"/> that owns this manager.</summary>
public SAVEditorViewModel Editor => _editor;
public SlotChangeManager(SAVEditorViewModel editor)
{
_editor = editor;
}
/// <summary>
/// Registers a <see cref="BoxViewerViewModel"/> so its slots can participate in drag-and-drop.
/// </summary>
public void RegisterBoxViewer(BoxViewerViewModel viewer) => _boxViewers.Add(viewer);
/// <summary>
/// Unregisters a <see cref="BoxViewerViewModel"/> when its window is closed.
/// </summary>
public void UnregisterBoxViewer(BoxViewerViewModel viewer) => _boxViewers.Remove(viewer);
/// <summary>
/// Initiates a drag operation from the given slot.
/// Call this from <see cref="Avalonia.Input.Pointer"/> 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;
}
/// <summary>
/// Resolved location of a <see cref="SlotModel"/> within the save file.
/// </summary>
private readonly record struct ResolvedSlot(int Box, int Index, bool IsParty, BoxViewerViewModel? BoxViewer);
/// <summary>
/// Resolves a <see cref="SlotModel"/> to its (box, index) coordinate by checking
/// the main editor's box/party slots and all registered box viewers.
/// </summary>
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;
}
/// <summary>
/// Gets the PKM at the resolved slot location.
/// </summary>
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);
}
/// <summary>
/// Executes the slot move/swap/clone operation.
/// </summary>
@ -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<SAVEditorViewModel.SlotChangeEntry>();
var entries = new List<SAVEditorViewModel.SlotChangeEntry>();
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);
}
/// <summary>
/// Refreshes box viewers whose slots were involved in a drag-drop operation.
/// </summary>
private void RefreshBoxViewers(SlotModel source, SlotModel dest)
{
foreach (var bv in _boxViewers)
{
if (bv.BoxSlots.Contains(source) || bv.BoxSlots.Contains(dest))
bv.RefreshBox();
}
}
/// <summary>
@ -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;
}
/// <summary>
@ -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);
}
/// <summary>
@ -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);
}
}
}

View File

@ -135,6 +135,11 @@ private void RefreshGameDataAfterLanguageChange()
/// </summary>
private PluginLoadResult? _pluginLoadResult;
/// <summary>
/// Plugin host that provides ISaveFileProvider to plugins.
/// </summary>
private AvaloniaPluginHost? _pluginHost;
public MainWindowViewModel() : this(new AvaloniaDialogService())
{
}
@ -168,15 +173,27 @@ public MainWindowViewModel(IDialogService dialogService)
/// </summary>
private void LoadPlugins()
{
if (!App.Settings.Startup.PluginLoadEnable)
return;
var pluginPath = Path.Combine(App.WorkingDirectory, "plugins");
try
{
_pluginLoadResult = PluginLoader.LoadPlugins<IPlugin>(pluginPath, Plugins, false);
_pluginLoadResult = PluginLoader.LoadPlugins<IPlugin>(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()
}
}
/// <summary>
/// Loads the initial save file and entity from startup arguments.
/// Called by App.axaml.cs after the main window is shown.
/// </summary>
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)

View File

@ -30,6 +30,12 @@ public partial class BoxViewerViewModel : ObservableObject
/// <summary>Callback invoked when a slot is clicked to view its PKM.</summary>
public Action<PKM>? SlotSelected { get; set; }
/// <summary>
/// Shared <see cref="SlotChangeManager"/> from the main editor, enabling drag-and-drop
/// between the BoxViewer and the main SAV editor.
/// </summary>
public SlotChangeManager? SlotManager { get; set; }
public BoxViewerViewModel(SaveFile sav, int initialBox = 0)
{
_sav = sav;

View File

@ -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";
/// <summary>
/// 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.
/// </summary>
public Action<string>? FileOpenRequested { get; set; }
/// <summary>
/// Set to true when a file has been opened (so the dialog can close).
/// </summary>
[ObservableProperty]
private bool _fileOpened;
/// <summary>
/// Creates the view model with the given folder paths and backup directory.
/// </summary>
public FolderListViewModel(string[] folderPaths, string backupPath)
public FolderListViewModel(string[] folderPaths, string backupPath, IReadOnlyList<string>? 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 */ }
}
/// <summary>
/// Opens the selected save file entry by invoking the <see cref="FileOpenRequested"/> callback.
/// Triggered by double-clicking a row in the Recent or Backup DataGrid.
/// </summary>
[RelayCommand]
private void OpenSaveFile(SaveFileEntry? entry)
{
if (entry is null || !File.Exists(entry.FilePath))
return;
FileOpenRequested?.Invoke(entry.FilePath);
FileOpened = true;
}
}

View File

@ -69,6 +69,7 @@ public WalkerCourseModel(int index, string name, bool unlocked)
/// </summary>
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<string> 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;
}
}

View File

@ -72,6 +72,7 @@ public PropModel(int index, string name, bool obtained)
/// </summary>
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;
}

View File

@ -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)

View File

@ -11,6 +11,7 @@ namespace PKHeX.Avalonia.ViewModels.Subforms;
/// </summary>
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;
}

View File

@ -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)
/// <summary>
/// Walks up the visual tree to find the <see cref="SAVEditorViewModel"/>.
/// Falls back to reaching it through <see cref="BoxViewerViewModel.SlotManager"/>
/// when the slot lives inside a BoxViewer window.
/// </summary>
private SAVEditorViewModel? FindSAVEditorViewModel()
{
var itemsControl = this.FindAncestorOfType<ItemsControl>();
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;
}
/// <summary>
/// Finds the <see cref="SlotChangeManager"/> from the <see cref="SAVEditorViewModel"/>.
/// Finds the <see cref="SlotChangeManager"/> from either the <see cref="SAVEditorViewModel"/>
/// or a <see cref="BoxViewerViewModel"/> ancestor.
/// </summary>
private SlotChangeManager? FindSlotChangeManager()
{
return FindSAVEditorViewModel()?.SlotManager;
var itemsControl = this.FindAncestorOfType<ItemsControl>();
if (itemsControl?.DataContext is SAVEditorViewModel savVm)
return savVm.SlotManager;
if (itemsControl?.DataContext is BoxViewerViewModel boxVm)
return boxVm.SlotManager;
return null;
}
}

View File

@ -47,11 +47,13 @@
<!-- File tabs -->
<TabControl Grid.Column="1" SelectedIndex="{Binding SelectedTabIndex}">
<TabItem Header="Recent">
<DataGrid ItemsSource="{Binding FilteredRecentFiles}"
<DataGrid x:Name="RecentGrid"
ItemsSource="{Binding FilteredRecentFiles}"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserSortColumns="True"
GridLinesVisibility="Horizontal">
GridLinesVisibility="Horizontal"
DoubleTapped="OnDataGridDoubleTapped">
<DataGrid.Columns>
<DataGridTextColumn Header="File Name" Binding="{Binding FileName}" Width="*" />
<DataGridTextColumn Header="Size" Binding="{Binding FileSize}" Width="100" />
@ -60,11 +62,13 @@
</DataGrid>
</TabItem>
<TabItem Header="Backups">
<DataGrid ItemsSource="{Binding FilteredBackupFiles}"
<DataGrid x:Name="BackupGrid"
ItemsSource="{Binding FilteredBackupFiles}"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserSortColumns="True"
GridLinesVisibility="Horizontal">
GridLinesVisibility="Horizontal"
DoubleTapped="OnDataGridDoubleTapped">
<DataGrid.Columns>
<DataGridTextColumn Header="File Name" Binding="{Binding FileName}" Width="*" />
<DataGridTextColumn Header="Size" Binding="{Binding FileSize}" Width="100" />

View File

@ -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);
}
}

View File

@ -79,7 +79,7 @@
VerticalAlignment="Center" IsVisible="{Binding HasSociability}"
FontWeight="SemiBold" FontSize="11" />
<NumericUpDown Grid.Row="6" Grid.Column="1" Value="{Binding Sociability}"
Minimum="0" Maximum="255" Width="120" Height="25" HorizontalAlignment="Left"
Minimum="0" Maximum="4294967295" Width="120" Height="25" HorizontalAlignment="Left"
IsVisible="{Binding HasSociability}" />
</Grid>
</StackPanel>