mirror of
https://github.com/kwsch/PKHeX.git
synced 2026-03-21 17:48:28 -05:00
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:
parent
970cbe6ce3
commit
875f7904ea
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user