Fix critical bugs: Level write-back, null safety, UX improvements

Critical bug fixes:
- Level now written back in PreparePKM (was causing data loss)
- Undo/Redo null safety on GetBoxSlotAtIndex result
- SKSurface.Create null check in UpdateLegality

UX improvements:
- Unsaved changes confirmation on window close
- Box mouse wheel navigation (scroll to change box)
- Tooltips: PID hex info, Species number, Nature stat effects
This commit is contained in:
montanon 2026-03-16 16:34:41 -03:00
parent 1d046b8459
commit a8b60cf4ab
6 changed files with 121 additions and 3 deletions

View File

@ -51,6 +51,12 @@ public partial class MainWindowViewModel : ObservableObject
[ObservableProperty]
private string _statusMessage = "Ready. Open a save file to begin.";
/// <summary>
/// Indicates whether the currently loaded save file has been modified since it was last saved or loaded.
/// </summary>
[ObservableProperty]
private bool _hasUnsavedChanges;
/// <summary>
/// The currently active UI language code, matching <see cref="GameLanguage"/> codes.
/// </summary>
@ -216,6 +222,7 @@ private async Task SaveFileAsync()
CreateAutoBackup(path);
ExportSAV(SaveFile, path);
HasUnsavedChanges = false;
StatusMessage = $"Saved to {Path.GetFileName(path)}";
}
catch (Exception ex)
@ -365,6 +372,7 @@ private void LoadSaveFile(SaveFile sav, string path)
{
SaveFile = sav;
HasSaveFile = true;
HasUnsavedChanges = true;
_loadedFilePath = path;
SpriteUtil.Initialize(sav);

View File

@ -62,6 +62,27 @@ public partial class PKMEditorViewModel : ObservableObject
UpdateSprite();
}
/// <summary>Tooltip showing the numeric species ID.</summary>
public string SpeciesTooltip => Entity is null ? "" : $"Species #{Entity.Species:000}";
/// <summary>
/// Tooltip showing which stats are raised/lowered by the current nature.
/// </summary>
public string NatureTooltip
{
get
{
var n = Nature;
var idx = (int)n;
if ((uint)idx >= 25) return n.ToString();
var up = idx / 5;
var down = idx % 5;
if (up == down) return $"{n} (Neutral)";
var statNames = new[] { "Atk", "Def", "Spe", "SpA", "SpD" };
return $"{n} (+{statNames[up]} / -{statNames[down]})";
}
}
// Stat Nature (Gen 8+)
[ObservableProperty] private Nature _statNature;
[ObservableProperty] private bool _hasStatNature;
@ -656,6 +677,7 @@ private void UpdateRegionNames()
try { Nickname = speciesName; }
finally { _isPopulating = false; }
}
OnPropertyChanged(nameof(SpeciesTooltip));
UpdateSprite();
UpdateLegality();
}
@ -672,6 +694,7 @@ private void UpdateRegionNames()
OnPropertyChanged(nameof(SpAColor));
OnPropertyChanged(nameof(SpDColor));
OnPropertyChanged(nameof(SpeColor));
OnPropertyChanged(nameof(NatureTooltip));
RecalcStats();
UpdateLegality();
}
@ -1545,6 +1568,7 @@ public void PopulateFields(PKM pk)
if (uint.TryParse(PidHex, System.Globalization.NumberStyles.HexNumber, null, out var pid))
Entity.PID = pid;
Entity.EXP = Exp;
Entity.Stat_Level = Level;
Entity.CurrentFriendship = (byte)Math.Clamp(Friendship, 0, 255);
Entity.Language = Language;
@ -2268,6 +2292,7 @@ private void UpdateLegality()
var color = valid ? SKColors.Green : SKColors.Red;
using var surface = SKSurface.Create(new SKImageInfo(24, 24));
if (surface is null) { LegalityImage = null; return; }
var canvas = surface.Canvas;
canvas.Clear(SKColors.Transparent);
using var paint = new SKPaint { Color = color, IsAntialias = true };

View File

@ -64,6 +64,7 @@ public void Undo()
var current = change.IsParty
? _sav.GetPartySlotAtIndex(change.Slot)
: _sav.GetBoxSlotAtIndex(change.Box, change.Slot);
if (current is null) return;
_redoStack.Push(new SlotChange(change.Box, change.Slot, current.DecryptedBoxData, change.IsParty));
// Restore old state
@ -92,6 +93,7 @@ public void Redo()
var current = change.IsParty
? _sav.GetPartySlotAtIndex(change.Slot)
: _sav.GetBoxSlotAtIndex(change.Box, change.Slot);
if (current is null) return;
_undoStack.Push(new SlotChange(change.Box, change.Slot, current.DecryptedBoxData, change.IsParty));
// Restore redo state

View File

@ -3,6 +3,9 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
using PKHeX.Avalonia.ViewModels;
@ -10,12 +13,72 @@ namespace PKHeX.Avalonia.Views;
public partial class MainWindow : Window
{
private bool _forceClose;
public MainWindow()
{
InitializeComponent();
AddHandler(DragDrop.DropEvent, OnDrop);
AddHandler(DragDrop.DragOverEvent, OnDragOver);
Closing += OnWindowClosing;
}
private async void OnWindowClosing(object? sender, WindowClosingEventArgs e)
{
if (_forceClose)
return;
if (DataContext is MainWindowViewModel vm && vm.HasUnsavedChanges)
{
e.Cancel = true;
var confirmDialog = new Window
{
Title = "Unsaved Changes",
Width = 360,
Height = 140,
CanResize = false,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
};
var result = false;
var cancelBtn = new Button { Content = "Cancel", Width = 80 };
cancelBtn.Click += (_, _) => { result = false; confirmDialog.Close(); };
var closeBtn = new Button { Content = "Close Anyway", Width = 100 };
closeBtn.Click += (_, _) => { result = true; confirmDialog.Close(); };
confirmDialog.Content = new StackPanel
{
Margin = new Thickness(20),
Spacing = 16,
Children =
{
new TextBlock
{
Text = "You have unsaved changes. Close without saving?",
TextWrapping = TextWrapping.Wrap,
},
new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Spacing = 8,
Children = { cancelBtn, closeBtn },
},
},
};
await confirmDialog.ShowDialog(this);
if (result)
{
_forceClose = true;
Close();
}
}
}
private void OnDragOver(object? sender, DragEventArgs e)

View File

@ -61,7 +61,7 @@
<!-- PID -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="PID:" TextAlignment="Right" VerticalAlignment="Center" Margin="0,0,6,0" />
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<TextBox Text="{Binding PidHex}" Height="25" Width="72" FontFamily="Courier New,Consolas,monospace" />
<TextBox Text="{Binding PidHex}" Height="25" Width="72" FontFamily="Courier New,Consolas,monospace" ToolTip.Tip="Personality ID (hex)" />
<Button Content="&#x2606;" Command="{Binding ShinytizeCommand}" Width="24" Height="25" Padding="0" FontSize="14" Margin="4,0,0,0" ToolTip.Tip="Toggle Shiny" />
<Button Content="Reroll" Command="{Binding RerollPidCommand}" Height="25" Padding="4,0" FontSize="9" Margin="2,0,0,0" />
</StackPanel>
@ -70,7 +70,8 @@
<TextBlock Grid.Row="1" Grid.Column="0" Text="Species:" TextAlignment="Right" VerticalAlignment="Center" Margin="0,0,6,0" />
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<ComboBox ItemsSource="{Binding SpeciesList}" SelectedItem="{Binding SelectedSpecies}"
DisplayMemberBinding="{Binding Text, DataType=core:ComboItem}" Height="25" Width="144" />
DisplayMemberBinding="{Binding Text, DataType=core:ComboItem}" Height="25" Width="144"
ToolTip.Tip="{Binding SpeciesTooltip}" />
<TextBlock Text="{Binding GenderSymbol}" FontSize="14" FontWeight="Bold"
VerticalAlignment="Center" Margin="6,0,0,0" Width="16" />
</StackPanel>
@ -96,7 +97,8 @@
<!-- Nature -->
<TextBlock Grid.Row="4" Grid.Column="0" Text="Nature:" TextAlignment="Right" VerticalAlignment="Center" Margin="0,0,6,0" />
<ComboBox Grid.Row="4" Grid.Column="1" ItemsSource="{Binding NatureList}" SelectedItem="{Binding SelectedNature}"
DisplayMemberBinding="{Binding Text, DataType=core:ComboItem}" Height="25" Width="144" HorizontalAlignment="Left" />
DisplayMemberBinding="{Binding Text, DataType=core:ComboItem}" Height="25" Width="144" HorizontalAlignment="Left"
ToolTip.Tip="{Binding NatureTooltip}" />
<!-- Stat Nature (Gen 8+) -->
<TextBlock Grid.Row="5" Grid.Column="0" Text="Stat Nature:" TextAlignment="Right" VerticalAlignment="Center" Margin="0,0,6,0"

View File

@ -1,4 +1,7 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using PKHeX.Avalonia.ViewModels;
namespace PKHeX.Avalonia.Views;
@ -7,5 +10,20 @@ public partial class SAVEditorView : UserControl
public SAVEditorView()
{
InitializeComponent();
// Tunnel handler so we intercept wheel events on the box area for box navigation
AddHandler(PointerWheelChangedEvent, OnBoxWheel, RoutingStrategies.Tunnel);
}
private void OnBoxWheel(object? sender, PointerWheelEventArgs e)
{
if (DataContext is SAVEditorViewModel vm && vm.IsLoaded)
{
if (e.Delta.Y > 0)
vm.PreviousBoxCommand.Execute(null);
else if (e.Delta.Y < 0)
vm.NextBoxCommand.Execute(null);
e.Handled = true;
}
}
}