PKHeX/PKHeX.Avalonia/ViewModels/Subforms/WondercardViewModel.cs
montanon 807007ad8a Fix bitmap memory leaks, load concurrency, money clamp
Bitmap disposal (6 files):
- SlotModel.SetImage: dispose old Avalonia Bitmap + input SKBitmap
- PKMEditorVM: dispose old SpriteImage, LegalityImage, BallSprite
  and intermediate SKBitmaps on every update
- SAVEditorVM: dispose old BoxWallpaper + SKBitmap on box navigation
- WondercardVM: dispose old GiftSlotModel.Sprite on refresh
- QRDialogVM: dispose intermediate SKBitmaps during QR generation
- Added ToAvaloniaBitmapAndDispose helper for owned SKBitmap conversion

Concurrency:
- MainWindowVM: add _isLoading guard to prevent concurrent LoadFileAsync
  calls from drag-drop or rapid Open clicks

Money clamp:
- Trainer8/8a/8b/9/9a: clamp Money to sav.MaxMoney on save
  (was allowing values exceeding game maximums)
2026-03-18 15:32:37 -03:00

138 lines
3.4 KiB
C#

using System.Collections.ObjectModel;
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PKHeX.Avalonia.Converters;
using PKHeX.Core;
using PKHeX.Drawing.Misc.Avalonia;
namespace PKHeX.Avalonia.ViewModels.Subforms;
/// <summary>
/// Model for a single mystery gift slot.
/// </summary>
public partial class GiftSlotModel : ObservableObject
{
public int Index { get; }
public DataMysteryGift Gift { get; private set; }
[ObservableProperty]
private Bitmap? _sprite;
[ObservableProperty]
private string _description = string.Empty;
[ObservableProperty]
private bool _isEmpty;
public GiftSlotModel(int index, DataMysteryGift gift)
{
Index = index;
Gift = gift;
Refresh();
}
public void Refresh()
{
IsEmpty = Gift.IsEmpty;
Description = IsEmpty ? $"Slot {Index + 1}: (empty)" : $"Slot {Index + 1}: {Gift.CardHeader}";
var skBitmap = Gift.Sprite();
var old = Sprite;
Sprite = SKBitmapToAvaloniaBitmapConverter.ToAvaloniaBitmapAndDispose(skBitmap);
old?.Dispose();
}
public void SetGift(DataMysteryGift gift)
{
Gift = gift;
Refresh();
}
}
/// <summary>
/// ViewModel for the Wondercard (Mystery Gift) editor subform.
/// </summary>
public partial class WondercardViewModel : SaveEditorViewModelBase
{
private readonly IMysteryGiftStorage _cards;
[ObservableProperty]
private GiftSlotModel? _selectedSlot;
[ObservableProperty]
private string _detailText = string.Empty;
/// <summary>All gift slot models.</summary>
public ObservableCollection<GiftSlotModel> Slots { get; } = [];
public WondercardViewModel(SaveFile sav) : base(sav)
{
_cards = GetMysteryGiftProvider(sav);
LoadSlots();
if (Slots.Count > 0)
SelectedSlot = Slots[0];
}
private void LoadSlots()
{
var count = _cards.GiftCountMax;
for (int i = 0; i < count; i++)
{
var gift = _cards.GetMysteryGift(i);
Slots.Add(new GiftSlotModel(i, gift));
}
}
partial void OnSelectedSlotChanged(GiftSlotModel? value)
{
if (value is null)
{
DetailText = string.Empty;
return;
}
if (value.Gift.IsEmpty)
{
DetailText = "(Empty slot)";
return;
}
var lines = value.Gift.GetDescription();
DetailText = string.Join("\n", lines);
}
/// <summary>
/// Deletes the currently selected gift slot.
/// </summary>
[RelayCommand]
private void DeleteSlot()
{
if (SelectedSlot is null)
return;
SelectedSlot.Gift.Clear();
SelectedSlot.Refresh();
OnSelectedSlotChanged(SelectedSlot);
}
/// <summary>
/// Saves all gift slots back to the save file.
/// </summary>
[RelayCommand]
private void Save()
{
for (int i = 0; i < Slots.Count && i < _cards.GiftCountMax; i++)
_cards.SetMysteryGift(i, Slots[i].Gift);
Modified = true;
}
private static IMysteryGiftStorage GetMysteryGiftProvider(SaveFile sav)
{
if (sav is IMysteryGiftStorageProvider provider)
return provider.MysteryGiftStorage;
throw new System.ArgumentException("Save file does not support Mystery Gifts.", nameof(sav));
}
}