diff --git a/PKHeX.Core/Editing/Saves/Slots/SlotChangelog.cs b/PKHeX.Core/Editing/Saves/Slots/SlotChangelog.cs
index 9cd103fba..10635a051 100644
--- a/PKHeX.Core/Editing/Saves/Slots/SlotChangelog.cs
+++ b/PKHeX.Core/Editing/Saves/Slots/SlotChangelog.cs
@@ -1,76 +1,214 @@
+using System;
using System.Collections.Generic;
+using System.Linq;
namespace PKHeX.Core;
///
-/// Tracks slot changes and provides the ability to revert a change.
+/// Maintains undo and redo history for changes made to a .
///
-public sealed class SlotChangelog(SaveFile SAV)
+public sealed record SlotChangelog(SaveFile Parent)
{
- private readonly Stack UndoStack = new();
- private readonly Stack RedoStack = new();
+ private readonly Stack _undo = new();
+ private readonly Stack _redo = new();
- public bool CanUndo => UndoStack.Count != 0;
- public bool CanRedo => RedoStack.Count != 0;
+ public bool CanUndo => _undo.Count != 0;
+ public bool CanRedo => _redo.Count != 0;
- public void AddNewChange(ISlotInfo info)
+ ///
+ /// Begins tracking a change affecting one slot.
+ ///
+ public Change Begin(ISlotInfo info) => Begin([info]);
+
+ ///
+ /// Begins tracking a change affecting multiple slots.
+ /// The resulting change is represented by a single undo/redo entry.
+ ///
+ public Change Begin(params IEnumerable slots)
{
- var revert = GetReversion(info, SAV);
- AddUndo(revert);
+ var reversion = CreateReversion(slots, Parent);
+ return new Change(this, reversion);
}
- public ISlotInfo Undo()
+ ///
+ /// Undoes the most recent committed change.
+ ///
+ /// The slots affected by the change.
+ public IReadOnlyList Undo()
{
- var change = UndoStack.Pop();
- var revert = GetReversion(change.Info, SAV);
- AddRedo(revert);
- change.Revert(SAV);
- return change.Info;
+ if (!CanUndo)
+ return [];
+
+ var change = _undo.Pop();
+
+ // Capture the state that exists immediately before the undo.
+ // That state becomes the redo operation.
+ var redo = change.CreateInverse(Parent);
+
+ change.Revert(Parent);
+ _redo.Push(redo);
+
+ return change.Slots;
}
- public ISlotInfo Redo()
+ ///
+ /// Redoes the most recently undone change.
+ ///
+ /// The slots affected by the change.
+ public IReadOnlyList Redo()
{
- var change = RedoStack.Pop();
- var revert = GetReversion(change.Info, SAV);
- AddUndo(revert);
- change.Revert(SAV);
- return change.Info;
+ if (!CanRedo)
+ return [];
+
+ var change = _redo.Pop();
+
+ // Capture the state that exists immediately before the redo.
+ // That state becomes the next undo operation.
+ var undo = change.CreateInverse(Parent);
+
+ change.Revert(Parent);
+ _undo.Push(undo);
+
+ return change.Slots;
}
- private void AddRedo(SlotReversion change)
+ private void Commit(ISlotReversion slotReversion)
{
- RedoStack.Push(change);
+ _undo.Push(slotReversion);
+ _redo.Clear();
}
- private void AddUndo(SlotReversion change)
+ // ReSharper disable once MemberCanBeMadeStatic.Local
+ // ReSharper disable once UnusedParameter.Local
+ private void Discard(ISlotReversion slotReversion)
{
- UndoStack.Push(change);
- RedoStack.Clear();
+ // A discarded change was never committed, so there is nothing to do.
+ // This method exists to keep Change's lifecycle explicit.
}
- private static SlotReversion GetReversion(ISlotInfo info, SaveFile sav) => info switch
+ private static ISlotReversion CreateReversion(IEnumerable slots, SaveFile parent)
{
- SlotInfoParty p => new PartyReversion(p, sav),
- _ => new SingleSlotReversion(info, sav),
+ var reversions = slots.Select(info => CreateReversion(info, parent)).ToArray();
+ return reversions.Length switch
+ {
+ 0 => throw new ArgumentException("At least one slot is required.", nameof(slots)),
+ 1 => reversions[0],
+ _ => new CompositeSlotReversion(reversions),
+ };
+ }
+
+ private static ISlotReversion CreateReversion(ISlotInfo info, SaveFile parent) => info switch
+ {
+ SlotInfoParty party => new PartySlotReversion(party, parent),
+ _ => new SlotSlotReversion(info, parent),
};
- private abstract class SlotReversion(ISlotInfo Info)
+ ///
+ /// Represents a change being prepared for commit.
+ /// Capture the change before modifying the save, then call
+ /// after the modification succeeds.
+ ///
+ public sealed class Change(SlotChangelog owner, ISlotReversion slotReversion) : IDisposable
{
- internal readonly ISlotInfo Info = Info;
- public abstract void Revert(SaveFile sav);
+ private bool _isCompleted;
+
+ ///
+ /// Commits the change to the undo history.
+ ///
+ public void Commit()
+ {
+ if (_isCompleted)
+ throw new InvalidOperationException("The change has already been completed.");
+
+ _isCompleted = true;
+ owner.Commit(slotReversion);
+ }
+
+ ///
+ /// Restores the state captured when the change began and discards it.
+ ///
+ public void Rollback()
+ {
+ if (_isCompleted)
+ throw new InvalidOperationException("The change has already been completed.");
+
+ _isCompleted = true;
+ slotReversion.Revert(owner.Parent);
+ owner.Discard(slotReversion);
+ }
+
+ ///
+ /// Discards the change without restoring anything.
+ /// Use this when no mutation was performed.
+ ///
+ public void Cancel()
+ {
+ if (_isCompleted)
+ throw new InvalidOperationException("The change has already been completed.");
+
+ _isCompleted = true;
+ owner.Discard(slotReversion);
+ }
+
+ public void Dispose()
+ {
+ // An uncommitted change is automatically rolled back.
+ if (!_isCompleted)
+ Rollback();
+ }
}
- private sealed class PartyReversion(ISlotInfo info, IList Party) : SlotReversion(info)
+ public interface ISlotReversion
{
- public PartyReversion(ISlotInfo info, SaveFile s) : this(info, s.PartyData) { }
-
- public override void Revert(SaveFile sav) => sav.PartyData = Party;
+ IReadOnlyList Slots { get; }
+ ISlotReversion CreateInverse(SaveFile parent);
+ void Revert(SaveFile parent);
}
- private sealed class SingleSlotReversion(ISlotInfo info, PKM Entity) : SlotReversion(info)
+ private sealed class SlotSlotReversion(ISlotInfo info, SaveFile parent) : ISlotReversion
{
- public SingleSlotReversion(ISlotInfo info, SaveFile sav) : this(info, info.Read(sav)) { }
+ private readonly PKM _entity = info.Read(parent).Clone();
+ public IReadOnlyList Slots => [info];
+ public ISlotReversion CreateInverse(SaveFile parent) => new SlotSlotReversion(info, parent);
+ public void Revert(SaveFile parent) => info.WriteTo(parent, _entity.Clone(), EntityImportSettings.None);
+ }
- public override void Revert(SaveFile sav) => Info.WriteTo(sav, Entity, EntityImportSettings.None);
+ private sealed class PartySlotReversion(SlotInfoParty info, SaveFile parent) : ISlotReversion
+ {
+ private readonly PKM[] _party = CloneParty(parent.PartyData);
+ public IReadOnlyList Slots => [info];
+ public ISlotReversion CreateInverse(SaveFile parent) => new PartySlotReversion(info, parent);
+ public void Revert(SaveFile parent) => parent.PartyData = CloneParty(_party);
+ private static PKM[] CloneParty(IEnumerable party) => [.. party.Select(pk => pk.Clone())];
+ }
+
+ private sealed class CompositeSlotReversion : ISlotReversion
+ {
+ private readonly IReadOnlyList Reversions;
+
+ public CompositeSlotReversion(params IReadOnlyList reversions)
+ {
+ if (reversions.Count == 0)
+ throw new ArgumentException("At least one reversion is required.", nameof(reversions));
+ Reversions = reversions;
+ }
+
+ public IReadOnlyList Slots => [.. Reversions.SelectMany(x => x.Slots)];
+
+ public ISlotReversion CreateInverse(SaveFile parent)
+ {
+ // Capture every inverse before modifying the save.
+ var inverse = Reversions
+ .Select(x => x.CreateInverse(parent))
+ .ToArray();
+
+ return new CompositeSlotReversion(inverse);
+ }
+
+ public void Revert(SaveFile parent)
+ {
+ foreach (var reversion in Reversions)
+ reversion.Revert(parent);
+ }
}
}
diff --git a/PKHeX.Core/Editing/Saves/Slots/SlotEditor.cs b/PKHeX.Core/Editing/Saves/Slots/SlotEditor.cs
index 8a7abb00e..db22ac4dc 100644
--- a/PKHeX.Core/Editing/Saves/Slots/SlotEditor.cs
+++ b/PKHeX.Core/Editing/Saves/Slots/SlotEditor.cs
@@ -1,3 +1,7 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
namespace PKHeX.Core;
///
@@ -10,6 +14,15 @@ public sealed class SlotEditor(SaveFile SAV)
private void NotifySlotChanged(ISlotInfo slot, SlotTouchType type, PKM pk) => Publisher.NotifySlotChanged(slot, type, pk);
+ ///
+ /// Notifies subscribers that a slot was modified externally.
+ ///
+ public void UpdateSlot(ISlotInfo slot)
+ {
+ var pk = slot.Read(SAV);
+ NotifySlotChanged(slot, SlotTouchType.Set, pk);
+ }
+
///
/// Gets data from a slot.
///
@@ -35,7 +48,12 @@ public SlotTouchResult Set(ISlotInfo slot, PKM pk, SlotTouchType type = SlotTouc
if (!slot.CanWriteTo(SAV))
return SlotTouchResult.FailWrite;
- WriteSlot(slot, pk, type);
+ using var change = Changelog.Begin(slot);
+ if (!slot.WriteTo(SAV, pk, EntityImportSettings.None))
+ return SlotTouchResult.FailWrite;
+
+ change.Commit();
+ NotifySlotChanged(slot, type, pk);
return SlotTouchResult.Success;
}
@@ -49,14 +67,21 @@ public SlotTouchResult Delete(ISlotInfo slot)
if (!slot.CanWriteTo(SAV))
return SlotTouchResult.FailDelete;
- if (!DeleteSlot(slot))
+ var pk = SAV.BlankPKM;
+ var settings = EntityImportSettings.None;
+
+ using var change = Changelog.Begin(slot);
+
+ if (!slot.WriteTo(SAV, pk, settings))
return SlotTouchResult.FailDelete;
+ change.Commit();
+ NotifySlotChanged(slot, SlotTouchType.Delete, pk);
return SlotTouchResult.Success;
}
///
- /// Swaps two slots.
+ /// Swaps two slots as one undoable operation.
///
/// Source slot to be switched with .
/// Destination slot to be switched with .
@@ -69,49 +94,93 @@ public SlotTouchResult Swap(ISlotInfo source, ISlotInfo dest)
return SlotTouchResult.FailDestination;
var settings = EntityImportSettings.None;
- var s = source.Read(SAV);
- var d = dest.Read(SAV);
- WriteSlot(source, s, SlotTouchType.None, settings);
- WriteSlot(dest, d, SlotTouchType.Swap, settings);
+
+ var sourcePK = source.Read(SAV);
+ var destPK = dest.Read(SAV);
+
+ using var change = Changelog.Begin([source, dest]);
+
+ if (!source.WriteTo(SAV, destPK, settings))
+ return SlotTouchResult.FailSource;
+
+ if (!dest.WriteTo(SAV, sourcePK, settings))
+ return SlotTouchResult.FailDestination;
+
+ change.Commit();
+
+ NotifySlotChanged(source, SlotTouchType.Swap, destPK);
+ NotifySlotChanged(dest, SlotTouchType.Swap, sourcePK);
return SlotTouchResult.Success;
}
- private bool WriteSlot(ISlotInfo slot, PKM pk, SlotTouchType type = SlotTouchType.Set, EntityImportSettings setDetail = default)
+ ///
+ /// Performs a batch operation against multiple slots as one undoable operation.
+ ///
+ /// Slots affected by the operation.
+ ///
+ /// Action which performs the actual modifications. The slots have already
+ /// been captured by the changelog when this action executes.
+ ///
+ public bool Batch(IEnumerable slots, Action> action)
{
- Changelog.AddNewChange(slot);
- var result = slot.WriteTo(SAV, pk, setDetail);
- if (result)
- NotifySlotChanged(slot, type, pk);
- return result;
- }
+ var affected = slots.ToArray();
- private bool DeleteSlot(ISlotInfo slot)
- {
- var pk = SAV.BlankPKM;
- var settings = EntityImportSettings.None;
- return WriteSlot(slot, pk, SlotTouchType.Delete, settings);
+ if (affected.Length == 0)
+ return false;
+
+ foreach (var slot in affected)
+ {
+ if (!slot.CanWriteTo(SAV))
+ return false;
+ }
+
+ using var change = Changelog.Begin(affected);
+
+ action(affected);
+ // Disposing `change` rolls back the captured state, even if the action throws.
+
+ change.Commit();
+ foreach (var slot in affected)
+ {
+ var pk = slot.Read(SAV);
+ NotifySlotChanged(slot, SlotTouchType.Set, pk);
+ }
+
+ return true;
}
///
- /// Undo the last change made to a slot.
+ /// Undoes the last change and notifies every affected slot.
///
public void Undo()
{
if (!Changelog.CanUndo)
return;
- var slot = Changelog.Undo();
- NotifySlotChanged(slot, SlotTouchType.Delete, slot.Read(SAV));
+
+ var slots = Changelog.Undo();
+
+ foreach (var slot in slots)
+ {
+ var pk = slot.Read(SAV);
+ NotifySlotChanged(slot, SlotTouchType.Undo, pk);
+ }
}
///
- /// Redo the last undone change made to a slot.
+ /// Redoes the last undone change and notifies every affected slot.
///
public void Redo()
{
if (!Changelog.CanRedo)
return;
- var slot = Changelog.Redo();
- NotifySlotChanged(slot, SlotTouchType.Delete, slot.Read(SAV));
+
+ var slots = Changelog.Redo();
+
+ foreach (var slot in slots)
+ {
+ var pk = slot.Read(SAV);
+ NotifySlotChanged(slot, SlotTouchType.Redo, pk);
+ }
}
}
diff --git a/PKHeX.Core/Editing/Saves/Slots/SlotTouchType.cs b/PKHeX.Core/Editing/Saves/Slots/SlotTouchType.cs
index c0190217c..c555f0a18 100644
--- a/PKHeX.Core/Editing/Saves/Slots/SlotTouchType.cs
+++ b/PKHeX.Core/Editing/Saves/Slots/SlotTouchType.cs
@@ -16,6 +16,10 @@ public enum SlotTouchType
Delete,
/// Data swap/move request
Swap,
+ /// Un-doing a previous modification
+ Undo,
+ /// Re-doing a modification that was previously un-done
+ Redo,
/// Request to be handled via external logic (atypical)
External,
diff --git a/PKHeX.WinForms/Controls/Slots/SlotUtil.cs b/PKHeX.WinForms/Controls/Slots/SlotUtil.cs
index 0f4c24868..f61afcdb4 100644
--- a/PKHeX.WinForms/Controls/Slots/SlotUtil.cs
+++ b/PKHeX.WinForms/Controls/Slots/SlotUtil.cs
@@ -21,6 +21,8 @@ public static class SlotUtil
SlotTouchType.Set => SpriteUtil.Spriter.Set,
SlotTouchType.Delete => SpriteUtil.Spriter.Delete,
SlotTouchType.Swap => SpriteUtil.Spriter.Set,
+ SlotTouchType.Undo => SpriteUtil.Spriter.Delete,
+ SlotTouchType.Redo => SpriteUtil.Spriter.Set,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
diff --git a/PKHeX.WinForms/MainWindow/Main.cs b/PKHeX.WinForms/MainWindow/Main.cs
index 4d87511a1..95b8ac306 100644
--- a/PKHeX.WinForms/MainWindow/Main.cs
+++ b/PKHeX.WinForms/MainWindow/Main.cs
@@ -431,10 +431,13 @@ private void MainMenuBoxDumpSingle(object sender, EventArgs e)
private void MainMenuBatchEditor(object sender, EventArgs e)
{
- using var form = new BatchEditor(PKME_Tabs.PreparePKM(), C_SAV.SAV);
- form.ShowDialog();
- C_SAV.SetPKMBoxes(); // refresh
- C_SAV.UpdateBoxViewers();
+ using var form = new BatchEditor(PKME_Tabs.PreparePKM(), C_SAV.SAV, C_SAV.EditEnv.Slots.Changelog);
+ if (form.ShowDialog() != DialogResult.OK)
+ return;
+
+ foreach (var slot in form.GetModifiedSlots())
+ C_SAV.EditEnv.Slots.UpdateSlot(slot);
+ C_SAV.UpdateUndoRedo();
}
private void MainMenuFolder(object sender, EventArgs e)
diff --git a/PKHeX.WinForms/Subforms/PKM Editors/BatchEditor.Designer.cs b/PKHeX.WinForms/Subforms/PKM Editors/BatchEditor.Designer.cs
index bc4d47c1d..0f7b822b1 100644
--- a/PKHeX.WinForms/Subforms/PKM Editors/BatchEditor.Designer.cs
+++ b/PKHeX.WinForms/Subforms/PKM Editors/BatchEditor.Designer.cs
@@ -13,9 +13,12 @@ partial class BatchEditor
/// true if managed resources should be disposed; otherwise, false.
protected override void Dispose(bool disposing)
{
- if (disposing && (components != null))
+ if (disposing)
{
- components.Dispose();
+ _filterCountCancellation?.Cancel();
+ _filterCountCancellation?.Dispose();
+ if (components != null)
+ components.Dispose();
}
base.Dispose(disposing);
}
@@ -34,11 +37,15 @@ private void InitializeComponent()
RB_Party = new System.Windows.Forms.RadioButton();
TB_Folder = new System.Windows.Forms.TextBox();
RTB_Instructions = new System.Windows.Forms.RichTextBox();
- B_Go = new System.Windows.Forms.Button();
- PB_Show = new System.Windows.Forms.ProgressBar();
+ B_Run = new System.Windows.Forms.Button();
+ B_Reset = new System.Windows.Forms.Button();
+ B_Cancel = new System.Windows.Forms.Button();
+ B_Save = new System.Windows.Forms.Button();
B_Add = new System.Windows.Forms.Button();
- b = new System.ComponentModel.BackgroundWorker();
+ TLP_Bottom = new System.Windows.Forms.TableLayoutPanel();
+ L_Count = new System.Windows.Forms.Label();
FLP_RB.SuspendLayout();
+ TLP_Bottom.SuspendLayout();
SuspendLayout();
//
// RB_Boxes
@@ -47,7 +54,7 @@ private void InitializeComponent()
RB_Boxes.Appearance = System.Windows.Forms.Appearance.Button;
RB_Boxes.AutoSize = true;
RB_Boxes.Checked = true;
- RB_Boxes.Location = new System.Drawing.Point(0, 1);
+ RB_Boxes.Location = new System.Drawing.Point(0, 0);
RB_Boxes.Margin = new System.Windows.Forms.Padding(0);
RB_Boxes.Name = "RB_Boxes";
RB_Boxes.Size = new System.Drawing.Size(52, 27);
@@ -62,7 +69,7 @@ private void InitializeComponent()
RB_Path.Anchor = System.Windows.Forms.AnchorStyles.Left;
RB_Path.Appearance = System.Windows.Forms.Appearance.Button;
RB_Path.AutoSize = true;
- RB_Path.Location = new System.Drawing.Point(99, 1);
+ RB_Path.Location = new System.Drawing.Point(99, 0);
RB_Path.Margin = new System.Windows.Forms.Padding(0);
RB_Path.Name = "RB_Path";
RB_Path.Size = new System.Drawing.Size(64, 27);
@@ -73,15 +80,16 @@ private void InitializeComponent()
//
// FLP_RB
//
- FLP_RB.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
+ TLP_Bottom.SetColumnSpan(FLP_RB, 5);
FLP_RB.Controls.Add(RB_Boxes);
FLP_RB.Controls.Add(RB_Party);
FLP_RB.Controls.Add(RB_Path);
FLP_RB.Controls.Add(TB_Folder);
- FLP_RB.Location = new System.Drawing.Point(12, 12);
- FLP_RB.Margin = new System.Windows.Forms.Padding(4);
+ FLP_RB.Dock = System.Windows.Forms.DockStyle.Fill;
+ FLP_RB.Location = new System.Drawing.Point(4, 4);
+ FLP_RB.Margin = new System.Windows.Forms.Padding(4, 4, 4, 0);
FLP_RB.Name = "FLP_RB";
- FLP_RB.Size = new System.Drawing.Size(460, 28);
+ FLP_RB.Size = new System.Drawing.Size(626, 28);
FLP_RB.TabIndex = 2;
//
// RB_Party
@@ -89,92 +97,162 @@ private void InitializeComponent()
RB_Party.Anchor = System.Windows.Forms.AnchorStyles.Left;
RB_Party.Appearance = System.Windows.Forms.Appearance.Button;
RB_Party.AutoSize = true;
- RB_Party.Location = new System.Drawing.Point(52, 1);
+ RB_Party.Location = new System.Drawing.Point(52, 0);
RB_Party.Margin = new System.Windows.Forms.Padding(0);
RB_Party.Name = "RB_Party";
RB_Party.Size = new System.Drawing.Size(47, 27);
RB_Party.TabIndex = 1;
RB_Party.Text = "Party";
RB_Party.UseVisualStyleBackColor = true;
+ RB_Party.Click += B_SAV_Click;
//
// TB_Folder
//
- TB_Folder.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
- TB_Folder.Location = new System.Drawing.Point(165, 2);
+ TB_Folder.Dock = System.Windows.Forms.DockStyle.Fill;
+ TB_Folder.Location = new System.Drawing.Point(2, 29);
TB_Folder.Margin = new System.Windows.Forms.Padding(2);
TB_Folder.Name = "TB_Folder";
TB_Folder.ReadOnly = true;
- TB_Folder.Size = new System.Drawing.Size(292, 25);
+ TB_Folder.Size = new System.Drawing.Size(465, 25);
TB_Folder.TabIndex = 3;
TB_Folder.Visible = false;
//
// RTB_Instructions
//
RTB_Instructions.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
- RTB_Instructions.Location = new System.Drawing.Point(12, 98);
- RTB_Instructions.Margin = new System.Windows.Forms.Padding(4);
+ TLP_Bottom.SetColumnSpan(RTB_Instructions, 5);
+ RTB_Instructions.Location = new System.Drawing.Point(4, 88);
+ RTB_Instructions.Margin = new System.Windows.Forms.Padding(4, 0, 4, 4);
RTB_Instructions.Name = "RTB_Instructions";
- RTB_Instructions.Size = new System.Drawing.Size(459, 177);
+ RTB_Instructions.Size = new System.Drawing.Size(626, 219);
RTB_Instructions.TabIndex = 5;
RTB_Instructions.Text = "";
+ RTB_Instructions.TextChanged += RTB_Instructions_TextChanged;
//
- // B_Go
+ // B_Run
//
- B_Go.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
- B_Go.Location = new System.Drawing.Point(405, 282);
- B_Go.Margin = new System.Windows.Forms.Padding(4);
- B_Go.Name = "B_Go";
- B_Go.Size = new System.Drawing.Size(66, 28);
- B_Go.TabIndex = 6;
- B_Go.Text = "Run";
- B_Go.UseVisualStyleBackColor = true;
- B_Go.Click += B_Go_Click;
+ B_Run.Dock = System.Windows.Forms.DockStyle.Fill;
+ B_Run.Location = new System.Drawing.Point(92, 315);
+ B_Run.Margin = new System.Windows.Forms.Padding(4);
+ B_Run.Name = "B_Run";
+ B_Run.Size = new System.Drawing.Size(80, 32);
+ B_Run.TabIndex = 9;
+ B_Run.Text = "Run";
+ B_Run.UseVisualStyleBackColor = true;
+ B_Run.Click += B_Run_Click;
//
- // PB_Show
+ // B_Reset
//
- PB_Show.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
- PB_Show.Location = new System.Drawing.Point(12, 284);
- PB_Show.Margin = new System.Windows.Forms.Padding(4);
- PB_Show.Name = "PB_Show";
- PB_Show.Size = new System.Drawing.Size(382, 24);
- PB_Show.TabIndex = 7;
+ B_Reset.Dock = System.Windows.Forms.DockStyle.Fill;
+ B_Reset.Enabled = false;
+ B_Reset.Location = new System.Drawing.Point(4, 315);
+ B_Reset.Margin = new System.Windows.Forms.Padding(4);
+ B_Reset.Name = "B_Reset";
+ B_Reset.Size = new System.Drawing.Size(80, 32);
+ B_Reset.TabIndex = 8;
+ B_Reset.Text = "Reset";
+ B_Reset.UseVisualStyleBackColor = true;
+ B_Reset.Click += B_Reset_Click;
+ //
+ // B_Cancel
+ //
+ B_Cancel.AutoSize = true;
+ B_Cancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
+ B_Cancel.Dock = System.Windows.Forms.DockStyle.Fill;
+ B_Cancel.Location = new System.Drawing.Point(462, 315);
+ B_Cancel.Margin = new System.Windows.Forms.Padding(4);
+ B_Cancel.Name = "B_Cancel";
+ B_Cancel.Size = new System.Drawing.Size(80, 32);
+ B_Cancel.TabIndex = 10;
+ B_Cancel.Text = "Cancel";
+ B_Cancel.UseVisualStyleBackColor = true;
+ B_Cancel.Click += B_Cancel_Click;
+ //
+ // B_Save
+ //
+ B_Save.AutoSize = true;
+ B_Save.Dock = System.Windows.Forms.DockStyle.Fill;
+ B_Save.Location = new System.Drawing.Point(550, 315);
+ B_Save.Margin = new System.Windows.Forms.Padding(4);
+ B_Save.Name = "B_Save";
+ B_Save.Size = new System.Drawing.Size(80, 32);
+ B_Save.TabIndex = 11;
+ B_Save.Text = "Save";
+ B_Save.UseVisualStyleBackColor = true;
+ B_Save.Click += B_Save_Click;
//
// B_Add
//
- B_Add.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
- B_Add.Location = new System.Drawing.Point(406, 42);
- B_Add.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
+ B_Add.Dock = System.Windows.Forms.DockStyle.Fill;
+ B_Add.Location = new System.Drawing.Point(550, 36);
+ B_Add.Margin = new System.Windows.Forms.Padding(4);
B_Add.Name = "B_Add";
- B_Add.Size = new System.Drawing.Size(66, 28);
+ B_Add.Size = new System.Drawing.Size(80, 48);
B_Add.TabIndex = 4;
B_Add.Text = "Add";
B_Add.UseVisualStyleBackColor = true;
B_Add.Click += B_Add_Click;
//
- // b
+ // TLP_Bottom
//
- b.WorkerReportsProgress = true;
+ TLP_Bottom.ColumnCount = 5;
+ TLP_Bottom.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 88F));
+ TLP_Bottom.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 88F));
+ TLP_Bottom.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
+ TLP_Bottom.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 88F));
+ TLP_Bottom.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 88F));
+ TLP_Bottom.Controls.Add(L_Count, 2, 3);
+ TLP_Bottom.Controls.Add(B_Add, 4, 1);
+ TLP_Bottom.Controls.Add(B_Save, 4, 3);
+ TLP_Bottom.Controls.Add(FLP_RB, 0, 0);
+ TLP_Bottom.Controls.Add(RTB_Instructions, 0, 2);
+ TLP_Bottom.Controls.Add(B_Reset, 0, 3);
+ TLP_Bottom.Controls.Add(B_Run, 1, 3);
+ TLP_Bottom.Controls.Add(B_Cancel, 3, 3);
+ TLP_Bottom.Dock = System.Windows.Forms.DockStyle.Fill;
+ TLP_Bottom.Location = new System.Drawing.Point(0, 0);
+ TLP_Bottom.Margin = new System.Windows.Forms.Padding(4);
+ TLP_Bottom.Name = "TLP_Bottom";
+ TLP_Bottom.RowCount = 4;
+ TLP_Bottom.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 32F));
+ TLP_Bottom.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 56F));
+ TLP_Bottom.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
+ TLP_Bottom.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 40F));
+ TLP_Bottom.Size = new System.Drawing.Size(634, 351);
+ TLP_Bottom.TabIndex = 12;
+ //
+ // L_Count
+ //
+ L_Count.Anchor = System.Windows.Forms.AnchorStyles.None;
+ L_Count.AutoSize = true;
+ L_Count.Location = new System.Drawing.Point(261, 322);
+ L_Count.Name = "L_Count";
+ L_Count.Size = new System.Drawing.Size(112, 17);
+ L_Count.TabIndex = 12;
+ L_Count.Text = "Matching: {0} / {1}";
//
// BatchEditor
//
AllowDrop = true;
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Inherit;
- ClientSize = new System.Drawing.Size(484, 321);
- Controls.Add(B_Add);
- Controls.Add(PB_Show);
- Controls.Add(B_Go);
- Controls.Add(RTB_Instructions);
- Controls.Add(FLP_RB);
+ CancelButton = B_Cancel;
+ ClientSize = new System.Drawing.Size(634, 351);
+ Controls.Add(TLP_Bottom);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
Icon = Properties.Resources.Icon;
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
MaximizeBox = false;
- MinimumSize = new System.Drawing.Size(500, 340);
+ MinimumSize = new System.Drawing.Size(650, 390);
Name = "BatchEditor";
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "Batch Editor";
+ FormClosing += BatchEditor_FormClosing;
+ DragDrop += TabMain_DragDrop;
+ DragEnter += TabMain_DragEnter;
FLP_RB.ResumeLayout(false);
FLP_RB.PerformLayout();
+ TLP_Bottom.ResumeLayout(false);
+ TLP_Bottom.PerformLayout();
ResumeLayout(false);
}
@@ -185,10 +263,13 @@ private void InitializeComponent()
private System.Windows.Forms.FlowLayoutPanel FLP_RB;
private System.Windows.Forms.TextBox TB_Folder;
private System.Windows.Forms.RichTextBox RTB_Instructions;
- private System.Windows.Forms.Button B_Go;
- private System.Windows.Forms.ProgressBar PB_Show;
+ private System.Windows.Forms.Button B_Run;
+ private System.Windows.Forms.Button B_Reset;
+ private System.Windows.Forms.Button B_Cancel;
+ private System.Windows.Forms.Button B_Save;
private System.Windows.Forms.Button B_Add;
private System.Windows.Forms.RadioButton RB_Party;
- private System.ComponentModel.BackgroundWorker b;
+ private System.Windows.Forms.TableLayoutPanel TLP_Bottom;
+ private System.Windows.Forms.Label L_Count;
}
}
diff --git a/PKHeX.WinForms/Subforms/PKM Editors/BatchEditor.cs b/PKHeX.WinForms/Subforms/PKM Editors/BatchEditor.cs
index 83d28c8b0..50e33f26f 100644
--- a/PKHeX.WinForms/Subforms/PKM Editors/BatchEditor.cs
+++ b/PKHeX.WinForms/Subforms/PKM Editors/BatchEditor.cs
@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using System.Windows.Forms;
using PKHeX.Core;
using PKHeX.WinForms.Controls;
@@ -11,60 +14,256 @@ namespace PKHeX.WinForms;
public partial class BatchEditor : Form
{
- private readonly SaveFile SAV;
+ private readonly SaveFile _sav;
+ private readonly SlotChangelog _changelog;
- // Mass Editing
- private EntityBatchProcessor editor = new();
- private readonly EntityInstructionBuilder UC_Builder;
+ // Cached source data. The cache is intentionally mutable; batch edits are accumulated here until the user chooses Save.
+ private IReadOnlyList? _boxData;
+ private IReadOnlyList? _party;
+ private IReadOnlyList? _folder;
+ private readonly Dictionary _folderPaths = new();
+ private readonly HashSet _modifiedSlots = [];
+ private readonly string _matchingCountFormat;
- private static string LastUsedCommands = string.Empty;
+ private EntityBatchProcessor _editor = new();
+ private readonly EntityInstructionBuilder _builder;
- public BatchEditor(PKM pk, SaveFile sav)
+ ///
+ /// Remember the last used commands so that they can be restored when the form is reopened.
+ ///
+ private static string _lastUsedCommands = string.Empty;
+
+ public BatchEditor(PKM pk, SaveFile sav, SlotChangelog changelog)
{
InitializeComponent();
WinFormsUtil.TranslateInterface(this, Main.CurrentLanguage);
- var above = FLP_RB.Location;
- UC_Builder = new EntityInstructionBuilder(() => pk)
- {
- Location = new() { Y = above.Y + FLP_RB.Height + 4 - 1, X = above.X + 1 },
- Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
- Width = B_Add.Location.X - above.X - 2,
- };
- Controls.Add(UC_Builder);
- SAV = sav;
- DragDrop += TabMain_DragDrop;
- DragEnter += TabMain_DragEnter;
+ _matchingCountFormat = L_Count.Text; // cache the translated string
+ _sav = sav;
+ _changelog = changelog;
- RTB_Instructions.Text = LastUsedCommands;
- FormClosing += (_, _) => LastUsedCommands = RTB_Instructions.Text;
+ // Builder needs to be late-bound to the input PKM from the main form.
+ _builder = new EntityInstructionBuilder(() => pk) { Dock = DockStyle.Fill, Margin = new Padding(4) };
+ TLP_Bottom.Controls.Add(_builder, 0, 1);
+ TLP_Bottom.SetColumnSpan(_builder, TLP_Bottom.ColumnCount - 1); // Add button occupies last column.
+
+ // Boxes are the default source and are immediately available for filter analysis.
+ _boxData = CreateBoxData();
+ UpdateFilterCountDebounced();
+ UpdateButtons();
+
+ RTB_Instructions.Text = _lastUsedCommands;
+ }
+
+ public IReadOnlyList GetModifiedSlots() => [.. _modifiedSlots];
+
+ private IReadOnlyList CreateBoxData()
+ {
+ var data = new List(_sav.SlotCount);
+ SlotInfoLoader.AddBoxData(_sav, data);
+ return data;
+ }
+
+ private IReadOnlyList CreatePartyData()
+ {
+ var data = new List(_sav.PartyCount);
+ SlotInfoLoader.AddPartyData(_sav, data);
+ return data;
+ }
+
+ private IReadOnlyList CreateFolderData()
+ {
+ if (!Directory.Exists(TB_Folder.Text))
+ return [];
+
+ var result = new List();
+ IEnumerable files;
+ try
+ {
+ files = Directory.GetFiles(TB_Folder.Text, "*", SearchOption.AllDirectories);
+ }
+ catch (IOException)
+ {
+ return result;
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return result;
+ }
+
+ foreach (var source in files)
+ {
+ var fi = new FileInfo(source);
+ if (!EntityDetection.IsSizePlausible(fi.Length))
+ continue;
+
+ try
+ {
+ var data = File.ReadAllBytes(source);
+ if (FileUtil.TryGetPKM(data, out var pk, fi.Extension, _sav))
+ {
+ var info = new SlotInfoFileSingle(source);
+ result.Add(new SlotCache(info, pk));
+ _folderPaths[info] = source;
+ }
+ }
+ catch (IOException)
+ {
+ // A file that cannot be read is simply not a processable source entity.
+ }
+ catch (UnauthorizedAccessException)
+ {
+ // A file that cannot be read is simply not a processable source entity.
+ }
+ }
+
+ return result;
+ }
+
+ private IReadOnlyList GetCurrentData()
+ {
+ if (RB_Party.Checked)
+ return _party ??= CreatePartyData();
+
+ if (RB_Path.Checked)
+ return _folder ??= CreateFolderData();
+
+ return _boxData!;
}
private void B_Open_Click(object sender, EventArgs e)
{
- if (!B_Go.Enabled)
- return;
using var fbd = new FolderBrowserDialog();
if (fbd.ShowDialog() != DialogResult.OK)
return;
TB_Folder.Text = fbd.SelectedPath;
TB_Folder.Visible = true;
+ RB_Path.Checked = true;
+ _folder = null;
+ _folderPaths.Clear();
+ UpdateFilterCountDebounced();
+ UpdateButtons();
}
private void B_SAV_Click(object sender, EventArgs e)
{
TB_Folder.Text = string.Empty;
TB_Folder.Visible = false;
+ _folder = null;
+ _folderPaths.Clear();
+ UpdateFilterCountDebounced();
+ UpdateButtons();
}
- private void B_Go_Click(object sender, EventArgs e)
+ private void B_Reset_Click(object sender, EventArgs e)
{
- RunBackgroundWorker();
+ // Reset only discards the in-memory save-file work. Folder operations have already
+ // been written to disk and intentionally cannot be reverted by this form.
+ _modifiedSlots.Clear();
+ _boxData = null;
+ _party = null;
+ _folder = null;
+ _folderPaths.Clear();
+ _editor = new EntityBatchProcessor();
+
+ RB_Boxes.Checked = true;
+ TB_Folder.Text = string.Empty;
+ TB_Folder.Visible = false;
+
+ _boxData = CreateBoxData();
+ UpdateFilterCountDebounced();
+ UpdateButtons();
+ }
+
+ private void B_Run_Click(object sender, EventArgs e)
+ {
+ ReadOnlySpan text = RTB_Instructions.Text;
+ if (!TryGetInstructionSets(text, out var sets, promptForEmptyValues: true, showErrors: true))
+ return;
+
+ foreach (var set in sets)
+ {
+ EntityBatchEditor.ScreenStrings(set.Filters);
+ EntityBatchEditor.ScreenStrings(set.Instructions);
+ }
+
+ if (RB_Path.Checked)
+ {
+ RunBatchEditFolder(sets);
+ return;
+ }
+
+ RunBatchEditSaveFile(sets);
+ }
+
+ private void B_Save_Click(object sender, EventArgs e)
+ {
+ if (_modifiedSlots.Count == 0)
+ {
+ DialogResult = DialogResult.OK;
+ return;
+ }
+
+ var slots = GetChangelogSlots();
+ using var change = _changelog.Begin(slots);
+ foreach (var slot in slots)
+ {
+ if (TryGetCachedSlot(slot, out var cache))
+ slot.WriteTo(_sav, cache.Entity, EntityImportSettings.None);
+ }
+
+ change.Commit();
+ DialogResult = DialogResult.OK;
+ }
+
+ private IReadOnlyList GetChangelogSlots()
+ {
+ // Party reversion captures the entire party, so multiple party slot entries only need
+ // one changelog slot. Box entries remain individually addressable.
+ var slots = _modifiedSlots.Where(z => z is not SlotInfoParty).ToList();
+ if (_modifiedSlots.Any(z => z is SlotInfoParty))
+ slots.Add(_modifiedSlots.First(z => z is SlotInfoParty));
+ return slots;
+ }
+
+ private bool TryGetCachedSlot(ISlotInfo source, out SlotCache cache)
+ {
+ if (_boxData is not null)
+ {
+ foreach (var slot in _boxData)
+ {
+ if (ReferenceEquals(slot.Source, source))
+ {
+ cache = slot;
+ return true;
+ }
+ }
+ }
+
+ if (_party is not null)
+ {
+ foreach (var slot in _party)
+ {
+ if (!ReferenceEquals(slot.Source, source))
+ continue;
+ cache = slot;
+ return true;
+ }
+ }
+
+ cache = null!;
+ return false;
+ }
+
+ private void B_Cancel_Click(object sender, EventArgs e)
+ {
+ DialogResult = DialogResult.Cancel;
+ Close();
}
private void B_Add_Click(object sender, EventArgs e)
{
- var s = UC_Builder.Create();
+ var s = _builder.Create();
if (s.Length == 0)
{ WinFormsUtil.Alert(MsgBEPropertyInvalid); return; }
@@ -76,17 +275,17 @@ private void B_Add_Click(object sender, EventArgs e)
RTB_Instructions.AppendText(s);
}
- private static void TabMain_DragEnter(object? sender, DragEventArgs? e)
+ private void TabMain_DragEnter(object? sender, DragEventArgs e)
{
- if (e?.Data is null)
+ if (e.Data is null)
return;
if (e.Data.GetDataPresent(DataFormats.FileDrop))
e.Effect = DragDropEffects.Copy;
}
- private void TabMain_DragDrop(object? sender, DragEventArgs? e)
+ private void TabMain_DragDrop(object? sender, DragEventArgs e)
{
- if (e?.Data?.GetData(DataFormats.FileDrop) is not string[] { Length: not 0 } files)
+ if (e.Data?.GetData(DataFormats.FileDrop) is not string[] { Length: not 0 } files)
return;
if (!Directory.Exists(files[0]))
return;
@@ -95,199 +294,251 @@ private void TabMain_DragDrop(object? sender, DragEventArgs? e)
TB_Folder.Visible = true;
RB_Boxes.Checked = RB_Party.Checked = false;
RB_Path.Checked = true;
+ _folder = null;
+ _folderPaths.Clear();
+ UpdateFilterCountDebounced();
+ UpdateButtons();
}
- private void RunBackgroundWorker()
+ private void RTB_Instructions_TextChanged(object? sender, EventArgs e) => UpdateFilterCountDebounced();
+ private CancellationTokenSource _filterCountCancellation = new();
+ private int _filterCountGeneration;
+
+ private async void UpdateFilterCountDebounced()
{
- ReadOnlySpan text = RTB_Instructions.Text;
+ try
+ {
+ var text = RTB_Instructions.Text;
+ await (_filterCountCancellation.CancelAsync());
+ _filterCountCancellation.Dispose();
+
+ var cancellation = new CancellationTokenSource();
+ _filterCountCancellation = cancellation;
+
+ var generation = ++_filterCountGeneration;
+ await Task.Delay(250, cancellation.Token);
+ if (cancellation.IsCancellationRequested)
+ return;
+
+ var result = await Task.Run(() => TryGetFilterMessage(text, cancellation.Token, out var message)
+ ? message
+ : null, cancellation.Token); // return to GUI thread
+
+ if (cancellation.IsCancellationRequested)
+ return;
+ if (generation != _filterCountGeneration)
+ return;
+ L_Count.Text = result;
+ }
+ catch
+ {
+ // Don't care.
+ }
+ }
+
+ private bool TryGetFilterMessage(ReadOnlySpan text, CancellationToken token, [NotNullWhen(true)] out string? result)
+ {
+ result = null;
+ var data = GetCurrentData();
+ int total = data.Count(z => z.Entity.Species != 0);
+ if (total == 0)
+ {
+ result = string.Format(_matchingCountFormat, 0, 0);
+ return true;
+ }
+
+ if (!TryGetInstructionSets(text, out var sets, promptForEmptyValues: false, allowOnlyFilters: true))
+ {
+ result = string.Format(_matchingCountFormat, "-", total);
+ return true;
+ }
+
+ foreach (var set in sets)
+ EntityBatchEditor.ScreenStrings(set.Filters);
+
+ if (token.IsCancellationRequested)
+ return false;
+
+ int matched = 0;
+ var max = _sav.MaxSpeciesID;
+ foreach (var entry in data)
+ {
+ var pk = entry.Entity;
+ if (pk.Species == 0 || pk.Species > max)
+ continue;
+ if (entry.Source is SlotInfoBox info && _sav.GetBoxSlotFlags(info.Box, info.Slot).IsOverwriteProtected())
+ continue;
+
+ if (token.IsCancellationRequested)
+ return false;
+
+ if (sets.Any(set => IsFilterMatch(entry, set)))
+ matched++;
+ }
+
+ result = string.Format(_matchingCountFormat, matched, total);
+ return true;
+ }
+
+ private static bool IsFilterMatch(SlotCache entry, StringInstructionSet set)
+ {
+ var filterMeta = set.Filters.Where(IsMetaFilter).ToArray();
+ var filters = set.Filters.Where(z => !IsMetaFilter(z)).ToArray();
+
+ if (!EntityBatchEditor.IsFilterMatchMeta(filterMeta, entry))
+ return false;
+
+ return filters.Length == 0 || BatchEditingUtil.IsFilterMatch(filters, entry.Entity);
+ }
+
+ private static bool IsMetaFilter(StringInstruction filter) => BatchFilters.FilterMeta.Any(z => z.IsMatch(filter.PropertyName));
+
+ private static bool TryGetInstructionSets(ReadOnlySpan text, out StringInstructionSet[] sets, bool promptForEmptyValues, bool showErrors = false, bool allowOnlyFilters = false)
+ {
+ sets = [];
+ if (text.IsEmpty)
+ return false;
if (StringInstructionSet.HasEmptyLine(text))
- { WinFormsUtil.Error(MsgBEInstructionInvalid); return; }
+ {
+ if (showErrors)
+ WinFormsUtil.Error(MsgBEInstructionInvalid);
+ return false;
+ }
+
+ try
+ {
+ sets = StringInstructionSet.GetBatchSets(text);
+ }
+ catch
+ {
+ if (showErrors)
+ WinFormsUtil.Error(MsgBEInstructionInvalid);
+ return false;
+ }
- var sets = StringInstructionSet.GetBatchSets(text);
if (Array.Exists(sets, s => s.Filters.Any(z => string.IsNullOrWhiteSpace(z.PropertyValue))))
- { WinFormsUtil.Error(MsgBEFilterEmpty); return; }
-
+ {
+ if (showErrors)
+ WinFormsUtil.Error(MsgBEFilterEmpty);
+ return false;
+ }
if (Array.Exists(sets, z => z.Instructions.Count == 0))
- { WinFormsUtil.Error(MsgBEInstructionNone); return; }
+ {
+ if (showErrors)
+ WinFormsUtil.Error(MsgBEInstructionNone);
+ return (allowOnlyFilters && sets.Any(z => z.Filters.Count != 0));
+ }
+
+ if (!promptForEmptyValues)
+ return true;
var emptyVal = sets.SelectMany(s => s.Instructions.Where(z => string.IsNullOrWhiteSpace(z.PropertyValue))).ToArray();
- if (emptyVal.Length != 0)
- {
- string props = string.Join(", ", emptyVal.Select(z => z.PropertyName));
- string invalid = MsgBEPropertyEmpty + Environment.NewLine + props;
- if (DialogResult.Yes != WinFormsUtil.Prompt(MessageBoxButtons.YesNo, invalid, MsgContinue))
- return;
- }
+ if (emptyVal.Length == 0)
+ return true;
- string? destPath = null;
- if (RB_Path.Checked)
- {
- WinFormsUtil.Alert(MsgExportFolder, MsgExportFolderAdvice);
- using var fbd = new FolderBrowserDialog();
- var dr = fbd.ShowDialog();
- if (dr != DialogResult.OK)
- return;
-
- destPath = fbd.SelectedPath;
- }
-
- FLP_RB.Enabled = RTB_Instructions.Enabled = B_Go.Enabled = false;
-
- foreach (var set in sets)
- {
- EntityBatchEditor.ScreenStrings(set.Filters);
- EntityBatchEditor.ScreenStrings(set.Instructions);
- }
- RunBatchEdit(sets, TB_Folder.Text, destPath);
+ string props = string.Join(", ", emptyVal.Select(z => z.PropertyName));
+ string invalid = MsgBEPropertyEmpty + Environment.NewLine + props;
+ return DialogResult.Yes == WinFormsUtil.Prompt(MessageBoxButtons.YesNo, invalid, MsgContinue);
}
- private void RunBatchEdit(StringInstructionSet[] sets, string source, string? destination)
- {
- editor = new EntityBatchProcessor();
- bool finished = false, displayed = false; // hack cuz DoWork event isn't cleared after completion
- b.DoWork += (_, _) =>
- {
- if (finished)
- return;
- if (RB_Boxes.Checked)
- RunBatchEditSaveFile(sets, boxes: true);
- else if (RB_Party.Checked)
- RunBatchEditSaveFile(sets, party: true);
- else if (destination is not null)
- RunBatchEditFolder(sets, source, destination);
- finished = true;
- };
- b.ProgressChanged += (_, e) => SetProgressBar(e.ProgressPercentage);
- b.RunWorkerCompleted += (_, _) =>
- {
- string result = editor.GetEditorResults(sets);
- if (!displayed) WinFormsUtil.Alert(result);
- displayed = true;
- FLP_RB.Enabled = RTB_Instructions.Enabled = B_Go.Enabled = true;
- SetupProgressBar(0);
- };
- b.RunWorkerAsync();
- }
-
- private void RunBatchEditFolder(IReadOnlyCollection sets, string source, string destination)
- {
- var files = Directory.GetFiles(source, "*", SearchOption.AllDirectories);
- SetupProgressBar(files.Length * sets.Count);
- foreach (var set in sets)
- ProcessFolder(files, destination, set.Filters, set.Instructions);
- }
-
- private void RunBatchEditSaveFile(IReadOnlyCollection sets, bool boxes = false, bool party = false)
- {
- if (party)
- {
- var data = new List(SAV.PartyCount);
- SlotInfoLoader.AddPartyData(SAV, data);
- process(data);
- foreach (var slot in data)
- slot.Source.WriteTo(SAV, slot.Entity, EntityImportSettings.None);
- }
- if (boxes)
- {
- var data = new List(SAV.SlotCount);
- SlotInfoLoader.AddBoxData(SAV, data);
- process(data);
- foreach (var slot in data)
- slot.Source.WriteTo(SAV, slot.Entity, EntityImportSettings.None);
- }
- void process(IList d)
- {
- SetupProgressBar(d.Count * sets.Count);
- foreach (var set in sets)
- ProcessSAV(d, set.Filters, set.Instructions);
- }
- }
-
- // Progress Bar
- private void SetupProgressBar(int count) => PB_Show.BeginInvoke(() =>
- {
- PB_Show.Minimum = 0;
- PB_Show.Step = 1;
- PB_Show.Value = 0;
- PB_Show.Maximum = count;
- });
-
- private void SetProgressBar(int position) => PB_Show.BeginInvoke(() => PB_Show.Value = position);
-
- private void ProcessSAV(IList data, IReadOnlyList Filters, IReadOnlyList Instructions)
+ private void RunBatchEditSaveFile(IReadOnlyCollection sets)
{
+ var data = GetCurrentData();
if (data.Count == 0)
return;
- // Pull out any filter meta instructions from the filters.
- var filterMeta = Filters.Where(f => BatchFilters.FilterMeta.Any(z => z.IsMatch(f.PropertyName))).ToArray();
+ _editor = new EntityBatchProcessor();
+ foreach (var set in sets)
+ ProcessSAV(data, set.Filters, set.Instructions);
+
+ UpdateFilterCountDebounced();
+ UpdateButtons();
+
+ string result = _editor.GetEditorResults(sets);
+ WinFormsUtil.Alert(result);
+ }
+
+ private void ProcessSAV(IReadOnlyList data, IReadOnlyList filters, IReadOnlyList instructions)
+ {
+ var filterMeta = filters.Where(IsMetaFilter).ToArray();
if (filterMeta.Length != 0)
- Filters = Filters.Except(filterMeta).ToArray();
+ filters = [.. filters.Where(z => !IsMetaFilter(z))];
- var max = SAV.MaxSpeciesID;
-
- for (int i = 0; i < data.Count; i++)
+ var max = _sav.MaxSpeciesID;
+ foreach (var entry in data)
{
- var entry = data[i];
var pk = entry.Entity;
-
- // Ignore empty/invalid slots.
var spec = pk.Species;
if (spec == 0 || spec > max)
- {
- b.ReportProgress(i);
continue;
- }
- if (entry.Source is SlotInfoBox info && SAV.GetBoxSlotFlags(info.Box, info.Slot).IsOverwriteProtected())
- editor.AddSkipped();
- else if (!EntityBatchEditor.IsFilterMatchMeta(filterMeta, entry))
- editor.AddSkipped();
- else
- editor.Process(pk, Filters, Instructions);
+ if (entry.Source is SlotInfoBox info && _sav.GetBoxSlotFlags(info.Box, info.Slot).IsOverwriteProtected())
+ continue;
+ if (!EntityBatchEditor.IsFilterMatchMeta(filterMeta, entry))
+ continue;
- b.ReportProgress(i);
+ if (_editor.Process(pk, filters, instructions))
+ _modifiedSlots.Add(entry.Source);
}
}
- private void ProcessFolder(IReadOnlyList files, string destDir, IReadOnlyList pkFilters, IReadOnlyList instructions)
+ private void RunBatchEditFolder(IReadOnlyCollection sets)
{
- var filterMeta = pkFilters.Where(f => BatchFilters.FilterMeta.Any(z => z.IsMatch(f.PropertyName))).ToArray();
+ if (string.IsNullOrWhiteSpace(TB_Folder.Text))
+ return;
+
+ WinFormsUtil.Alert(MsgExportFolder, MsgExportFolderAdvice);
+ using var fbd = new FolderBrowserDialog();
+ if (fbd.ShowDialog() != DialogResult.OK)
+ return;
+
+ var data = GetCurrentData();
+ if (data.Count == 0)
+ return;
+
+ var destination = fbd.SelectedPath;
+ _editor = new EntityBatchProcessor();
+ foreach (var set in sets)
+ ProcessFolder(data, destination, set.Filters, set.Instructions);
+
+ string result = _editor.GetEditorResults(sets);
+ WinFormsUtil.Alert(result);
+ UpdateFilterCountDebounced();
+ }
+
+ private void ProcessFolder(IReadOnlyList data, string destDir, IReadOnlyList pkFilters, IReadOnlyList instructions)
+ {
+ var filterMeta = pkFilters.Where(IsMetaFilter).ToArray();
if (filterMeta.Length != 0)
- pkFilters = pkFilters.Except(filterMeta).ToArray();
+ pkFilters = [.. pkFilters.Where(z => !IsMetaFilter(z))];
- for (int i = 0; i < files.Count; i++)
+ Span maxEntity = stackalloc byte[0x800]; // lol too big, futureproof for now
+ foreach (var entry in data)
{
- TryProcess(files[i], destDir, filterMeta, pkFilters, instructions);
- b.ReportProgress(i);
- }
- }
+ if (!EntityBatchEditor.IsFilterMatchMeta(filterMeta, entry))
+ continue;
- private void TryProcess(string source, string destDir, IReadOnlyList metaFilters, IReadOnlyList pkFilters, IReadOnlyList instructions)
- {
- var fi = new FileInfo(source);
- if (!EntityDetection.IsSizePlausible(fi.Length))
- return;
+ if (!_editor.Process(entry.Entity, pkFilters, instructions))
+ continue;
- byte[] data = File.ReadAllBytes(source);
- _ = FileUtil.TryGetPKM(data, out var pk, fi.Extension, SAV);
- if (pk is null)
- return;
+ if (!_folderPaths.TryGetValue(entry.Source, out var source))
+ continue;
- var info = new SlotInfoFileSingle(source);
- var entry = new SlotCache(info, pk);
- if (!EntityBatchEditor.IsFilterMatchMeta(metaFilters, entry))
- {
- editor.AddSkipped();
- return;
- }
-
- if (editor.Process(pk, pkFilters, instructions))
- {
- Span result = stackalloc byte[pk.SIZE_PARTY];
- pk.ForcePartyData();
- pk.WriteDecryptedDataParty(result);
+ // We might have mixed size files, so we can't have a shared stackalloc
+ var result = maxEntity[..entry.Entity.SIZE_PARTY];
+ entry.Entity.ForcePartyData();
+ entry.Entity.WriteDecryptedDataParty(result);
File.WriteAllBytes(Path.Combine(destDir, Path.GetFileName(source)), result);
}
}
+
+ private void UpdateButtons()
+ {
+ bool isOperatingOnFolder = RB_Path.Checked;
+ B_Run.Enabled = RTB_Instructions.Text.Length != 0 && (isOperatingOnFolder || GetCurrentData().Count(z => z.Entity.Species != 0) != 0);
+ B_Save.Enabled = isOperatingOnFolder || _modifiedSlots.Count != 0;
+ B_Reset.Enabled = _modifiedSlots.Count != 0;
+ }
+
+ private void BatchEditor_FormClosing(object? sender, FormClosingEventArgs e) => _lastUsedCommands = RTB_Instructions.Text;
}
diff --git a/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_DonutGenerator9a.Designer.cs b/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_DonutGenerator9a.Designer.cs
index 4dcf714c2..b780bfc90 100644
--- a/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_DonutGenerator9a.Designer.cs
+++ b/PKHeX.WinForms/Subforms/Save Editors/Gen9/SAV_DonutGenerator9a.Designer.cs
@@ -116,7 +116,6 @@ private void InitializeComponent()
//
// SAV_DonutGenerator9a
//
- AcceptButton = B_Generate;
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Inherit;
CancelButton = B_Cancel;
ClientSize = new System.Drawing.Size(536, 232);