Use undo and invalidation batching where appropriate.

Fixes: Inserting frames, cloning frames, and Lua edits could result in large numbers of undo actions.
Also fixes enabled state of undo and redo menu items.
This commit is contained in:
SuuperW 2025-07-05 03:06:59 -05:00
parent 4fedc62c51
commit 2c3eb93ea8
No known key found for this signature in database
5 changed files with 96 additions and 95 deletions

View File

@ -314,8 +314,10 @@ namespace BizHawk.Client.EmuHawk
_luaLibsImpl.IsUpdateSupressed = true;
Tastudio.ApiHawkBatchEdit(() =>
Tastudio.StopRecordingOnNextEdit = false;
Tastudio.CurrentTasMovie.SingleInvalidation(() =>
{
Tastudio.CurrentTasMovie.ChangeLog.BeginNewBatch("tastudio.applyinputchanges");
int size = _changeList.Count;
for (int i = 0; i < size; i++)
@ -345,6 +347,7 @@ namespace BizHawk.Client.EmuHawk
}
}
_changeList.Clear();
Tastudio.CurrentTasMovie.ChangeLog.EndBatch();
});
_luaLibsImpl.IsUpdateSupressed = false;

View File

@ -27,7 +27,6 @@ namespace BizHawk.Client.EmuHawk
private int _startRow;
private int _batchEditMinFrame = -1;
private bool _batchEditing;
private bool _editIsFromLua;
// Editing analog input
private string _axisEditColumn = "";
@ -405,10 +404,13 @@ namespace BizHawk.Client.EmuHawk
else
{
BoolPatterns[ControllerType.BoolButtons.IndexOf(buttonName)].Reset();
foreach (var index in TasView.SelectedRows)
CurrentTasMovie.SingleInvalidation(() =>
{
CurrentTasMovie.SetBoolState(index, buttonName, BoolPatterns[ControllerType.BoolButtons.IndexOf(buttonName)].GetNextValue());
}
foreach (var index in TasView.SelectedRows)
{
CurrentTasMovie.SetBoolState(index, buttonName, BoolPatterns[ControllerType.BoolButtons.IndexOf(buttonName)].GetNextValue());
}
});
}
}
else
@ -783,22 +785,6 @@ namespace BizHawk.Client.EmuHawk
return false;
}
public void ApiHawkBatchEdit(Action action)
{
// This is only caled from Lua.
_editIsFromLua = true;
BeginBatchEdit();
try
{
action();
}
finally
{
EndBatchEdit();
_editIsFromLua = false;
}
}
/// <summary>
/// Disables recording mode, ensures we are in the greenzone, and does autorestore if needed.
/// If a mouse button is down, only tracks the edit so we can do this stuff on mouse up.
@ -828,11 +814,12 @@ namespace BizHawk.Client.EmuHawk
}
else
{
if (!_editIsFromLua)
if (StopRecordingOnNextEdit)
{
// Lua users will want to preserve recording mode.
TastudioPlayMode(true);
}
StopRecordingOnNextEdit = true;
if (Emulator.Frame > frame)
{
@ -1098,6 +1085,30 @@ namespace BizHawk.Client.EmuHawk
}
else if (_rightClickFrame != -1)
{
FramePaint(frame, startVal, endVal);
}
// Left-click
else if (TasView.IsPaintDown && !string.IsNullOrEmpty(_startBoolDrawColumn))
{
BoolPaint(frame, startVal, endVal);
}
else if (TasView.IsPaintDown && !string.IsNullOrEmpty(_startAxisDrawColumn))
{
AxisPaint(frame, startVal, endVal);
}
CurrentTasMovie.IsCountingRerecords = wasCountingRerecords;
if (MouseButtonHeld)
{
TasView.MakeIndexVisible(TasView.CurrentCell.RowIndex.Value); // todo: limit scrolling speed
SetTasViewRowCount(); // refreshes
}
}
private void FramePaint(int frame, int startVal, int endVal)
{
CurrentTasMovie.SingleInvalidation(() => {
if (frame > CurrentTasMovie.InputLogLength - _rightClickInput.Length)
{
frame = CurrentTasMovie.InputLogLength - _rightClickInput.Length;
@ -1201,11 +1212,12 @@ namespace BizHawk.Client.EmuHawk
{
_suppressContextMenu = true;
}
}
});
}
// Left-click
else if (TasView.IsPaintDown && !string.IsNullOrEmpty(_startBoolDrawColumn))
{
private void BoolPaint(int frame, int startVal, int endVal)
{
CurrentTasMovie.SingleInvalidation(() => {
CurrentTasMovie.IsCountingRerecords = false;
for (int i = startVal; i <= endVal; i++) // Inclusive on both ends (drawing up or down)
@ -1226,9 +1238,12 @@ namespace BizHawk.Client.EmuHawk
CurrentTasMovie.SetBoolState(i, _startBoolDrawColumn, setVal); // Notice it uses new row, old column, you can only paint across a single column
}
}
});
}
else if (TasView.IsPaintDown && !string.IsNullOrEmpty(_startAxisDrawColumn))
private void AxisPaint(int frame, int startVal, int endVal)
{
CurrentTasMovie.SingleInvalidation(() =>
{
CurrentTasMovie.IsCountingRerecords = false;
@ -1249,15 +1264,7 @@ namespace BizHawk.Client.EmuHawk
CurrentTasMovie.SetAxisState(i, _startAxisDrawColumn, setVal); // Notice it uses new row, old column, you can only paint across a single column
}
}
CurrentTasMovie.IsCountingRerecords = wasCountingRerecords;
if (MouseButtonHeld)
{
TasView.MakeIndexVisible(TasView.CurrentCell.RowIndex.Value); // todo: limit scrolling speed
SetTasViewRowCount(); // refreshes
}
});
}
private void TasView_MouseMove(object sender, MouseEventArgs e)
@ -1338,6 +1345,7 @@ namespace BizHawk.Client.EmuHawk
return;
}
// TODO: properly handle axis editing batches
BeginBatchEdit();
int value = CurrentTasMovie.GetAxisState(_axisEditRow, _axisEditColumn);

View File

@ -277,38 +277,21 @@ namespace BizHawk.Client.EmuHawk
GreenzoneICheckSeparator.Visible =
StateHistoryIntegrityCheckMenuItem.Visible =
VersionInfo.DeveloperBuild;
UndoMenuItem.Enabled = CurrentTasMovie.ChangeLog.CanUndo;
RedoMenuItem.Enabled = CurrentTasMovie.ChangeLog.CanRedo;
}
private void UndoMenuItem_Click(object sender, EventArgs e)
{
if (CurrentTasMovie.ChangeLog.Undo() < Emulator.Frame)
{
GoToFrame(CurrentTasMovie.ChangeLog.PreviousUndoFrame);
}
else
{
RefreshDialog();
}
// Currently I don't have a way to easily detect when CanUndo changes, so this button should be enabled always.
// UndoMenuItem.Enabled = CurrentTasMovie.ChangeLog.CanUndo;
RedoMenuItem.Enabled = CurrentTasMovie.ChangeLog.CanRedo;
CurrentTasMovie.ChangeLog.Undo();
RefreshDialog(); // ?? redundant, except if undoing markers
}
private void RedoMenuItem_Click(object sender, EventArgs e)
{
if (CurrentTasMovie.ChangeLog.Redo() < Emulator.Frame)
{
GoToFrame(CurrentTasMovie.ChangeLog.PreviousRedoFrame);
}
else
{
RefreshDialog();
}
// Currently I don't have a way to easily detect when CanUndo changes, so this button should be enabled always.
// UndoMenuItem.Enabled = CurrentTasMovie.ChangeLog.CanUndo;
RedoMenuItem.Enabled = CurrentTasMovie.ChangeLog.CanRedo;
CurrentTasMovie.ChangeLog.Redo();
RefreshDialog(); // ?? redundant, except if undoing markers
}
private void ShowUndoHistoryMenuItem_Click(object sender, EventArgs e)
@ -495,9 +478,10 @@ namespace BizHawk.Client.EmuHawk
private void ClearFramesMenuItem_Click(object sender, EventArgs e)
{
if (TasView.Focused && TasView.AnyRowsSelected)
if (!TasView.Focused || !TasView.AnyRowsSelected) return;
CurrentTasMovie.SingleInvalidation(() =>
{
BeginBatchEdit();
CurrentTasMovie.ChangeLog.BeginNewBatch($"Clear frames {TasView.SelectionStartIndex}-{TasView.SelectionEndIndex}");
foreach (int frame in TasView.SelectedRows)
{
@ -505,8 +489,7 @@ namespace BizHawk.Client.EmuHawk
}
CurrentTasMovie.ChangeLog.EndBatch();
EndBatchEdit();
}
});
}
private void DeleteFramesMenuItem_Click(object sender, EventArgs e)
@ -544,22 +527,28 @@ namespace BizHawk.Client.EmuHawk
private void CloneFramesXTimes(int timesToClone)
{
BeginBatchEdit();
for (int i = 0; i < timesToClone; i++)
if (!TasView.Focused || !TasView.AnyRowsSelected) return;
var framesToInsert = TasView.SelectedRows;
var insertionFrame = Math.Min((TasView.SelectionEndIndex ?? 0) + 1, CurrentTasMovie.InputLogLength);
var inputLog = framesToInsert
.Select(CurrentTasMovie.GetInputLogEntry)
.ToList();
CurrentTasMovie.SingleInvalidation(() =>
{
if (TasView.Focused && TasView.AnyRowsSelected)
string batchName = $"Clone {inputLog.Count} frames starting at {TasView.FirstSelectedRowIndex}";
if (timesToClone != 1) batchName += $" {timesToClone} times";
CurrentTasMovie.ChangeLog.BeginNewBatch(batchName);
for (int i = 0; i < timesToClone; i++)
{
var framesToInsert = TasView.SelectedRows;
var insertionFrame = Math.Min((TasView.SelectionEndIndex ?? 0) + 1, CurrentTasMovie.InputLogLength);
var inputLog = framesToInsert
.Select(frame => CurrentTasMovie.GetInputLogEntry(frame))
.ToList();
CurrentTasMovie.InsertInput(insertionFrame, inputLog);
}
}
EndBatchEdit();
CurrentTasMovie.ChangeLog.EndBatch();
});
}
private void InsertFrameMenuItem_Click(object sender, EventArgs e)

View File

@ -64,6 +64,11 @@ namespace BizHawk.Client.EmuHawk
[ConfigPersist]
public Font TasViewFont { get; set; } = new Font("Arial", 8.25F, FontStyle.Bold, GraphicsUnit.Point, 0);
/// <summary>
/// This is meant to be used by Lua.
/// </summary>
public bool StopRecordingOnNextEdit = true;
public class TAStudioSettings
{
public TAStudioSettings()

View File

@ -85,19 +85,17 @@ namespace BizHawk.Client.EmuHawk
private void UndoToHere(int index)
{
int earliestFrame = int.MaxValue;
while (Log.UndoIndex > index)
_tastudio.CurrentTasMovie.SingleInvalidation(() =>
{
int frame = Log.Undo();
if (frame < earliestFrame)
earliestFrame = frame;
}
while (Log.UndoIndex > index)
{
Log.Undo();
}
});
UpdateValues();
// potentially rewind, then update display for TAStudio
if (_tastudio.Emulator.Frame > earliestFrame)
_tastudio.GoToFrame(earliestFrame);
// potentially redundant refresh
_tastudio.RefreshDialog();
}
@ -136,19 +134,17 @@ namespace BizHawk.Client.EmuHawk
private void RedoHereMenuItem_Click(object sender, EventArgs e)
{
int earliestFrame = int.MaxValue;
while (Log.UndoIndex < SelectedItem)
_tastudio.CurrentTasMovie.SingleInvalidation(() =>
{
int frame = Log.Redo();
if (earliestFrame < frame)
earliestFrame = frame;
}
while (Log.UndoIndex < SelectedItem)
{
Log.Redo();
}
});
UpdateValues();
// potentially rewind, then update display for TAStudio
if (_tastudio.Emulator.Frame > earliestFrame)
_tastudio.GoToFrame(earliestFrame);
// potentially redundant refresh
_tastudio.RefreshDialog();
}