/* * Undo.cpp * -------- * Purpose: Editor undo buffer functionality. * Notes : (currently none) * Authors: Olivier Lapicque * OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #include "stdafx.h" #include "Moddoc.h" #include "Mainfrm.h" #include "Undo.h" #include "../common/mptStringBuffer.h" #include "../tracklib/SampleEdit.h" #include "../soundlib/modsmp_ctrl.h" OPENMPT_NAMESPACE_BEGIN ///////////////////////////////////////////////////////////////////////////////////////// // Pattern Undo Functions // Remove all undo steps. void CPatternUndo::ClearUndo() { UndoBuffer.clear(); RedoBuffer.clear(); } // Create undo point. // Parameter list: // - pattern: Pattern of which an undo step should be created from. // - firstChn: first channel, 0-based. // - firstRow: first row, 0-based. // - numChns: width // - numRows: height // - description: Short description text of action for undo menu. // - linkToPrevious: Don't create a separate undo step, but link this to the previous undo event. Use this for commands that modify several patterns at once. // - storeChannelInfo: Also store current channel header information (pan / volume / etc. settings) and number of channels in this undo point. bool CPatternUndo::PrepareUndo(PATTERNINDEX pattern, CHANNELINDEX firstChn, ROWINDEX firstRow, CHANNELINDEX numChns, ROWINDEX numRows, const char *description, bool linkToPrevious, bool storeChannelInfo) { if(PrepareBuffer(UndoBuffer, pattern, firstChn, firstRow, numChns, numRows, description, linkToPrevious, storeChannelInfo)) { RedoBuffer.clear(); return true; } return false; } bool CPatternUndo::PrepareChannelUndo(CHANNELINDEX firstChn, CHANNELINDEX numChns, const char *description) { return PrepareUndo(PATTERNINDEX_INVALID, firstChn, 0, numChns, 0, description, false, true); } bool CPatternUndo::PrepareBuffer(undobuf_t &buffer, PATTERNINDEX pattern, CHANNELINDEX firstChn, ROWINDEX firstRow, CHANNELINDEX numChns, ROWINDEX numRows, const char *description, bool linkToPrevious, bool storeChannelInfo) const { const CSoundFile &sndFile = modDoc.GetSoundFile(); const bool onlyChannelInfo = storeChannelInfo && numRows < 1; if(storeChannelInfo && pattern != PATTERNINDEX_INVALID && firstChn == 0 && numChns != sndFile.GetNumChannels()) { numChns = sndFile.GetNumChannels(); } ROWINDEX patRows = 0; if(sndFile.Patterns.IsValidPat(pattern)) { patRows = sndFile.Patterns[pattern].GetNumRows(); if((firstRow >= patRows) || (firstChn >= sndFile.GetNumChannels())) return false; if(numChns < 1 || numRows < 1) return false; if(firstRow + numRows >= patRows) numRows = patRows - firstRow; if(firstChn + numChns >= sndFile.GetNumChannels()) numChns = sndFile.GetNumChannels() - firstChn; } else if(!onlyChannelInfo) { return false; } // Remove an undo step if there are too many. if(buffer.size() >= MAX_UNDO_LEVEL) { buffer.erase(buffer.begin(), buffer.begin() + (buffer.size() - MAX_UNDO_LEVEL + 1)); } UndoInfo undo; undo.pattern = pattern; undo.numPatternRows = patRows; undo.firstChannel = firstChn; undo.firstRow = firstRow; undo.numChannels = numChns; undo.numRows = numRows; undo.linkToPrevious = linkToPrevious; undo.description = description; if(!onlyChannelInfo) { try { undo.content.resize(numRows * numChns); } catch(mpt::out_of_memory e) { mpt::delete_out_of_memory(e); return false; } const ModCommand *pPattern = sndFile.Patterns[pattern].GetpModCommand(firstRow, firstChn); auto pUndoData = undo.content.begin(); for(ROWINDEX iy = 0; iy < numRows; iy++) { std::copy(pPattern, pPattern + numChns, pUndoData); pUndoData += numChns; pPattern += sndFile.GetNumChannels(); } } if(storeChannelInfo) { undo.channelInfo.assign(std::begin(sndFile.ChnSettings) + firstChn, std::begin(sndFile.ChnSettings) + firstChn + numChns); } buffer.push_back(std::move(undo)); if(!linkToPrevious) modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); return true; } // Restore an undo point. Returns which pattern has been modified. PATTERNINDEX CPatternUndo::Undo() { return Undo(UndoBuffer, RedoBuffer, false); } // Restore an undo point. Returns which pattern has been modified. PATTERNINDEX CPatternUndo::Redo() { return Undo(RedoBuffer, UndoBuffer, false); } // Restore an undo point. Returns which pattern has been modified. // linkedFromPrevious is true if a connected undo event is going to be deleted (can only be called internally). PATTERNINDEX CPatternUndo::Undo(undobuf_t &fromBuf, undobuf_t &toBuf, bool linkedFromPrevious) { CSoundFile &sndFile = modDoc.GetSoundFile(); bool linkToPrevious = false; if(fromBuf.empty()) return PATTERNINDEX_INVALID; // Select most recent undo slot const UndoInfo &undo = fromBuf.back(); const bool onlyChannelSettings = undo.OnlyChannelSettings(); const bool deletePattern = (undo.numPatternRows == DELETE_PATTERN) && !onlyChannelSettings; // Add this action to redo buffer if the pattern exists; otherwise add a special deletion redo step later const bool patternExists = sndFile.Patterns.IsValidPat(undo.pattern); if(patternExists || onlyChannelSettings) PrepareBuffer(toBuf, undo.pattern, undo.firstChannel, undo.firstRow, undo.numChannels, undo.numRows, undo.description, linkedFromPrevious, !undo.channelInfo.empty()); const bool modifyChannels = !undo.channelInfo.empty(); const CHANNELINDEX updateChannel = (undo.numChannels == 1) ? undo.firstChannel : CHANNELINDEX_INVALID; if(modifyChannels) { const bool modifyChannelCount = (undo.pattern != PATTERNINDEX_INVALID && undo.channelInfo.size() != sndFile.GetNumChannels()) || (undo.pattern == PATTERNINDEX_INVALID && (undo.firstChannel + undo.channelInfo.size()) > sndFile.GetNumChannels()); if(modifyChannelCount) { // Add or remove channels std::vector channels(undo.channelInfo.size(), CHANNELINDEX_INVALID); const CHANNELINDEX copyCount = std::min(sndFile.GetNumChannels(), static_cast(undo.channelInfo.size())); std::iota(channels.begin(), channels.begin() + copyCount, CHANNELINDEX(0)); modDoc.ReArrangeChannels(channels, false); } if(undo.firstChannel + undo.channelInfo.size() <= sndFile.GetNumChannels()) { std::move(undo.channelInfo.cbegin(), undo.channelInfo.cend(), std::begin(sndFile.ChnSettings) + undo.firstChannel); } // Channel mute status might have changed... for(CHANNELINDEX i = undo.firstChannel; i < sndFile.GetNumChannels(); i++) { modDoc.UpdateChannelMuteStatus(i); } } PATTERNINDEX pat = undo.pattern; if(deletePattern) { sndFile.Patterns.Remove(pat); } else if(undo.firstChannel + undo.numChannels <= sndFile.GetNumChannels() && !onlyChannelSettings) { if(!patternExists) { if(!sndFile.Patterns.Insert(pat, undo.numPatternRows)) { fromBuf.pop_back(); return PATTERNINDEX_INVALID; } } else if(sndFile.Patterns[pat].GetNumRows() != undo.numPatternRows) { sndFile.Patterns[pat].Resize(undo.numPatternRows); } linkToPrevious = undo.linkToPrevious; auto pUndoData = undo.content.cbegin(); CPattern &pattern = sndFile.Patterns[pat]; ModCommand *m = pattern.GetpModCommand(undo.firstRow, undo.firstChannel); const ROWINDEX numRows = std::min(undo.numRows, pattern.GetNumRows()); for(ROWINDEX iy = 0; iy < numRows; iy++) { std::move(pUndoData, pUndoData + undo.numChannels, m); m += sndFile.GetNumChannels(); pUndoData += undo.numChannels; } } if(!patternExists && !onlyChannelSettings) { // Redo a deletion auto &redo = fromBuf.back(); redo.content.clear(); redo.numPatternRows = DELETE_PATTERN; toBuf.push_back(std::move(redo)); } fromBuf.pop_back(); if(patternExists != sndFile.Patterns.IsValidPat(pat)) { modDoc.UpdateAllViews(nullptr, PatternHint(pat).Names().Undo()); modDoc.UpdateAllViews(nullptr, SequenceHint().Data()); // Pattern color will change in sequence } else { modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); } if(modifyChannels) modDoc.UpdateAllViews(nullptr, GeneralHint(updateChannel).Channels()); modDoc.SetModified(); if(linkToPrevious) { pat = Undo(fromBuf, toBuf, true); } return pat; } // Public helper function to remove the most recent undo point. void CPatternUndo::RemoveLastUndoStep() { if(UndoBuffer.empty()) return; UndoBuffer.pop_back(); modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); } CString CPatternUndo::GetName(const undobuf_t &buffer) { if(buffer.empty()) return CString(); const UndoInfo &info = buffer.back(); CString desc = mpt::ToCString(mpt::Charset::Locale, info.description); if(info.linkToPrevious) desc += _T(" (Multiple Patterns)"); else if(info.OnlyChannelSettings() && info.numChannels > 1) desc += _T(" (Multiple Channels)"); else if(info.OnlyChannelSettings()) desc += MPT_CFORMAT(" (Channel {})")(info.firstChannel + 1); else desc += MPT_CFORMAT(" (Pat {} Row {} Chn {})")(info.pattern, info.firstRow, info.firstChannel + 1); return desc; } void CPatternUndo::RearrangePatterns(undobuf_t &buffer, const std::vector &newIndex) { for(auto &step : buffer) { if(step.pattern < newIndex.size()) step.pattern = newIndex[step.pattern]; } } void CPatternUndo::RearrangePatterns(const std::vector &newIndex) { RearrangePatterns(UndoBuffer, newIndex); RearrangePatterns(RedoBuffer, newIndex); } ///////////////////////////////////////////////////////////////////////////////////////// // Sample Undo Functions // Remove all undo steps for all samples. void CSampleUndo::ClearUndo() { for(SAMPLEINDEX smp = 1; smp <= MAX_SAMPLES; smp++) { ClearUndo(UndoBuffer, smp); ClearUndo(RedoBuffer, smp); } UndoBuffer.clear(); RedoBuffer.clear(); } // Remove all undo steps of a given sample. void CSampleUndo::ClearUndo(undobuf_t &buffer, const SAMPLEINDEX smp) { if(!SampleBufferExists(buffer, smp)) return; while(!buffer[smp - 1].empty()) { DeleteStep(buffer, smp, 0); } } // Create undo point for given sample. // The main program has to tell what kind of changes are going to be made to the sample. // That way, a lot of RAM can be saved, because some actions don't even require an undo sample buffer. bool CSampleUndo::PrepareUndo(const SAMPLEINDEX smp, sampleUndoTypes changeType, const char *description, SmpLength changeStart, SmpLength changeEnd) { if(PrepareBuffer(UndoBuffer, smp, changeType, description, changeStart, changeEnd)) { ClearUndo(RedoBuffer, smp); return true; } return false; } bool CSampleUndo::PrepareBuffer(undobuf_t &buffer, const SAMPLEINDEX smp, sampleUndoTypes changeType, const char *description, SmpLength changeStart, SmpLength changeEnd) { if(smp == 0 || smp >= MAX_SAMPLES) return false; if(!TrackerSettings::Instance().m_SampleUndoBufferSize.Get().GetSizeInBytes()) { // Undo/Redo is disabled return false; } if(smp > buffer.size()) { buffer.resize(smp); } // Remove an undo step if there are too many. while(buffer[smp - 1].size() >= MAX_UNDO_LEVEL) { DeleteStep(buffer, smp, 0); } // Create new undo slot UndoInfo undo; const CSoundFile &sndFile = modDoc.GetSoundFile(); const ModSample &oldSample = sndFile.GetSample(smp); // Save old sample header undo.OldSample = oldSample; undo.oldName = sndFile.m_szNames[smp]; undo.changeType = changeType; undo.description = description; if(changeType == sundo_replace) { // ensure that size information is correct here. changeStart = 0; changeEnd = oldSample.nLength; } else if(changeType == sundo_none) { // we do nothing... changeStart = changeEnd = 0; } if(changeStart > oldSample.nLength || changeStart > changeEnd) { // Something is surely screwed up. MPT_ASSERT(false); return false; } // Restrict amount of memory that's being used RestrictBufferSize(); undo.changeStart = changeStart; undo.changeEnd = changeEnd; undo.samplePtr = nullptr; switch(changeType) { case sundo_none: // we are done, no sample changes here. case sundo_invert: // no action necessary, since those effects can be applied again to be undone. case sundo_reverse: // ditto case sundo_unsign: // ditto case sundo_insert: // no action necessary, we already have stored the variables that are necessary. break; case sundo_update: case sundo_delete: case sundo_replace: if(oldSample.HasSampleData()) { const uint8 bytesPerSample = oldSample.GetBytesPerSample(); const SmpLength changeLen = changeEnd - changeStart; undo.samplePtr = ModSample::AllocateSample(changeLen, bytesPerSample); if(undo.samplePtr == nullptr) return false; memcpy(undo.samplePtr, oldSample.sampleb() + changeStart * bytesPerSample, changeLen * bytesPerSample); #ifdef MPT_ALL_LOGGING const size_t nSize = (GetBufferCapacity(UndoBuffer) + GetBufferCapacity(RedoBuffer) + changeLen * bytesPerSample) >> 10; MPT_LOG_GLOBAL(LogDebug, "Undo", MPT_UFORMAT("Sample undo/redo buffer size is now {}.{} MB")(nSize >> 10, (nSize & 1023) * 100 / 1024)); #endif } break; default: MPT_ASSERT(false); // whoops, what's this? someone forgot to implement it, some code is obviously missing here! return false; } buffer[smp - 1].push_back(std::move(undo)); modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); return true; } // Restore undo point for given sample bool CSampleUndo::Undo(const SAMPLEINDEX smp) { return Undo(UndoBuffer, RedoBuffer, smp); } // Restore redo point for given sample bool CSampleUndo::Redo(const SAMPLEINDEX smp) { return Undo(RedoBuffer, UndoBuffer, smp); } // Restore undo/redo point for given sample bool CSampleUndo::Undo(undobuf_t &fromBuf, undobuf_t &toBuf, const SAMPLEINDEX smp) { if(!SampleBufferExists(fromBuf, smp) || fromBuf[smp - 1].empty()) return false; CSoundFile &sndFile = modDoc.GetSoundFile(); // Select most recent undo slot and temporarily remove it from the buffer so that it won't get deleted by possible buffer size restrictions in PrepareBuffer() UndoInfo undo = fromBuf[smp - 1].back(); fromBuf[smp - 1].pop_back(); // When turning an undo point into a redo point (and vice versa), some action types need to be adjusted. sampleUndoTypes redoType = undo.changeType; if(redoType == sundo_delete) redoType = sundo_insert; else if(redoType == sundo_insert) redoType = sundo_delete; PrepareBuffer(toBuf, smp, redoType, undo.description, undo.changeStart, undo.changeEnd); ModSample &sample = sndFile.GetSample(smp); std::byte *pCurrentSample = mpt::void_cast(sample.samplev()); int8 *pNewSample = nullptr; // a new sample is possibly going to be allocated, depending on what's going to be undone. bool keepOnDisk = sample.uFlags[SMP_KEEPONDISK]; bool replace = false; uint8 bytesPerSample = undo.OldSample.GetBytesPerSample(); SmpLength changeLen = undo.changeEnd - undo.changeStart; switch(undo.changeType) { case sundo_none: break; case sundo_invert: // invert again SampleEdit::InvertSample(sample, undo.changeStart, undo.changeEnd, sndFile); break; case sundo_reverse: // reverse again SampleEdit::ReverseSample(sample, undo.changeStart, undo.changeEnd, sndFile); break; case sundo_unsign: // unsign again SampleEdit::UnsignSample(sample, undo.changeStart, undo.changeEnd, sndFile); break; case sundo_insert: // delete inserted data MPT_ASSERT(changeLen == sample.nLength - undo.OldSample.nLength); if(undo.OldSample.nLength > 0) { memcpy(pCurrentSample + undo.changeStart * bytesPerSample, pCurrentSample + undo.changeEnd * bytesPerSample, (sample.nLength - undo.changeEnd) * bytesPerSample); // also clean the sample end memset(pCurrentSample + undo.OldSample.nLength * bytesPerSample, 0, (sample.nLength - undo.OldSample.nLength) * bytesPerSample); } else { replace = true; } break; case sundo_update: // simply replace what has been updated. if(sample.nLength < undo.changeEnd) return false; memcpy(pCurrentSample + undo.changeStart * bytesPerSample, undo.samplePtr, changeLen * bytesPerSample); break; case sundo_delete: // insert deleted data pNewSample = static_cast(ModSample::AllocateSample(undo.OldSample.nLength, bytesPerSample)); if(pNewSample == nullptr) return false; replace = true; memcpy(pNewSample, pCurrentSample, undo.changeStart * bytesPerSample); memcpy(pNewSample + undo.changeStart * bytesPerSample, undo.samplePtr, changeLen * bytesPerSample); memcpy(pNewSample + undo.changeEnd * bytesPerSample, pCurrentSample + undo.changeStart * bytesPerSample, (undo.OldSample.nLength - undo.changeEnd) * bytesPerSample); break; case sundo_replace: // simply exchange sample pointer pNewSample = static_cast(undo.samplePtr); undo.samplePtr = nullptr; // prevent sample from being deleted replace = true; break; default: MPT_ASSERT(false); // whoops, what's this? someone forgot to implement it, some code is obviously missing here! return false; } // Restore old sample header sample = undo.OldSample; sample.pData.pSample = mpt::void_cast(pCurrentSample); // select the "correct" old sample sndFile.m_szNames[smp] = undo.oldName; if(replace) { ctrlSmp::ReplaceSample(sample, pNewSample, undo.OldSample.nLength, sndFile); } sample.PrecomputeLoops(sndFile, true); if(undo.changeType != sundo_none) { sample.uFlags.set(SMP_MODIFIED); } if(!keepOnDisk) { // Never re-enable the keep on disk flag after it was disabled. // This can lead to quite some dangerous situations when replacing samples. sample.uFlags.reset(SMP_KEEPONDISK); } fromBuf[smp - 1].push_back(std::move(undo)); DeleteStep(fromBuf, smp, fromBuf[smp - 1].size() - 1); modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); modDoc.SetModified(); return true; } // Delete a given undo / redo step of a sample. void CSampleUndo::DeleteStep(undobuf_t &buffer, const SAMPLEINDEX smp, const size_t step) { if(!SampleBufferExists(buffer, smp) || step >= buffer[smp - 1].size()) return; ModSample::FreeSample(buffer[smp - 1][step].samplePtr); buffer[smp - 1].erase(buffer[smp - 1].begin() + step); } // Public helper function to remove the most recent undo point. void CSampleUndo::RemoveLastUndoStep(const SAMPLEINDEX smp) { if(!CanUndo(smp)) return; DeleteStep(UndoBuffer, smp, UndoBuffer[smp - 1].size() - 1); modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); } // Restrict undo buffer size so it won't grow too large. // This is done in FIFO style, equally distributed over all sample slots (very simple). void CSampleUndo::RestrictBufferSize() { size_t capacity = GetBufferCapacity(UndoBuffer) + GetBufferCapacity(RedoBuffer); while(capacity > TrackerSettings::Instance().m_SampleUndoBufferSize.Get().GetSizeInBytes()) { RestrictBufferSize(UndoBuffer, capacity); RestrictBufferSize(RedoBuffer, capacity); } } void CSampleUndo::RestrictBufferSize(undobuf_t &buffer, size_t &capacity) { for(SAMPLEINDEX smp = 1; smp <= buffer.size(); smp++) { if(capacity <= TrackerSettings::Instance().m_SampleUndoBufferSize.Get().GetSizeInBytes()) return; for(size_t i = 0; i < buffer[smp - 1].size(); i++) { if(buffer[smp - 1][i].samplePtr != nullptr) { capacity -= (buffer[smp - 1][i].changeEnd - buffer[smp - 1][i].changeStart) * buffer[smp - 1][i].OldSample.GetBytesPerSample(); for(size_t j = 0; j <= i; j++) { DeleteStep(buffer, smp, 0); } // Try to evenly spread out the restriction, i.e. move on to other samples before deleting another step for this sample. break; } } } } // Update undo buffer when using rearrange sample functionality. // newIndex contains one new index for each old index. newIndex[1] represents the first sample. void CSampleUndo::RearrangeSamples(undobuf_t &buffer, const std::vector &newIndex) { undobuf_t newBuf(modDoc.GetNumSamples()); const SAMPLEINDEX newSize = static_cast(newIndex.size()); const SAMPLEINDEX oldSize = static_cast(buffer.size()); for(SAMPLEINDEX smp = 1; smp <= oldSize; smp++) { MPT_ASSERT(smp >= newSize || newIndex[smp] <= modDoc.GetNumSamples()); if(smp < newSize && newIndex[smp] > 0 && newIndex[smp] <= modDoc.GetNumSamples()) { newBuf[newIndex[smp] - 1] = buffer[smp - 1]; } else { ClearUndo(smp); } } #ifdef _DEBUG for(size_t i = 0; i < oldSize; i++) { if(i + 1 < newIndex.size() && newIndex[i + 1] != 0) MPT_ASSERT(newBuf[newIndex[i + 1] - 1].size() == buffer[i].size()); else MPT_ASSERT(buffer[i].empty()); } #endif buffer = newBuf; } // Return total amount of bytes used by the sample undo buffer. size_t CSampleUndo::GetBufferCapacity(const undobuf_t &buffer) const { size_t sum = 0; for(auto &smp : buffer) { for(auto &step : smp) { if(step.samplePtr != nullptr) { sum += (step.changeEnd - step.changeStart) * step.OldSample.GetBytesPerSample(); } } } return sum; } // Ensure that the undo buffer is big enough for a given sample number bool CSampleUndo::SampleBufferExists(const undobuf_t &buffer, const SAMPLEINDEX smp) const { if(smp == 0 || smp >= MAX_SAMPLES) return false; if(smp <= buffer.size()) return true; return false; } // Get name of next undo item const char *CSampleUndo::GetUndoName(const SAMPLEINDEX smp) const { if(!CanUndo(smp)) { return ""; } return UndoBuffer[smp - 1].back().description; } // Get name of next redo item const char *CSampleUndo::GetRedoName(const SAMPLEINDEX smp) const { if(!CanRedo(smp)) { return ""; } return RedoBuffer[smp - 1].back().description; } ///////////////////////////////////////////////////////////////////////////////////////// // Instrument Undo Functions // Remove all undo steps for all instruments. void CInstrumentUndo::ClearUndo() { UndoBuffer.clear(); RedoBuffer.clear(); } // Remove all undo steps of a given instrument. void CInstrumentUndo::ClearUndo(undobuf_t &buffer, const INSTRUMENTINDEX ins) { if(!InstrumentBufferExists(buffer, ins)) return; buffer[ins - 1].clear(); } // Create undo point for given Instrument. // The main program has to tell what kind of changes are going to be made to the Instrument. // That way, a lot of RAM can be saved, because some actions don't even require an undo Instrument buffer. bool CInstrumentUndo::PrepareUndo(const INSTRUMENTINDEX ins, const char *description, EnvelopeType envType) { if(PrepareBuffer(UndoBuffer, ins, description, envType)) { ClearUndo(RedoBuffer, ins); return true; } return false; } bool CInstrumentUndo::PrepareBuffer(undobuf_t &buffer, const INSTRUMENTINDEX ins, const char *description, EnvelopeType envType) { if(ins == 0 || ins >= MAX_INSTRUMENTS || modDoc.GetSoundFile().Instruments[ins] == nullptr) return false; if(ins > buffer.size()) { buffer.resize(ins); } auto &insBuffer = buffer[ins - 1]; // Remove undo steps if there are too many. if(insBuffer.size() >= MAX_UNDO_LEVEL) { insBuffer.erase(insBuffer.begin(), insBuffer.begin() + (insBuffer.size() - MAX_UNDO_LEVEL + 1)); } // Create new undo slot UndoInfo undo; const CSoundFile &sndFile = modDoc.GetSoundFile(); undo.description = description; undo.editedEnvelope = envType; if(envType < ENV_MAXTYPES) { undo.instr.GetEnvelope(envType) = sndFile.Instruments[ins]->GetEnvelope(envType); } else { undo.instr = *sndFile.Instruments[ins]; } // cppcheck false-positive // cppcheck-suppress uninitStructMember insBuffer.push_back(std::move(undo)); modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); return true; } // Restore undo point for given Instrument bool CInstrumentUndo::Undo(const INSTRUMENTINDEX ins) { return Undo(UndoBuffer, RedoBuffer, ins); } // Restore redo point for given Instrument bool CInstrumentUndo::Redo(const INSTRUMENTINDEX ins) { return Undo(RedoBuffer, UndoBuffer, ins); } // Restore undo/redo point for given Instrument bool CInstrumentUndo::Undo(undobuf_t &fromBuf, undobuf_t &toBuf, const INSTRUMENTINDEX ins) { CSoundFile &sndFile = modDoc.GetSoundFile(); if(sndFile.Instruments[ins] == nullptr || !InstrumentBufferExists(fromBuf, ins) || fromBuf[ins - 1].empty()) return false; // Select most recent undo slot const UndoInfo &undo = fromBuf[ins - 1].back(); PrepareBuffer(toBuf, ins, undo.description, undo.editedEnvelope); // When turning an undo point into a redo point (and vice versa), some action types need to be adjusted. ModInstrument *instr = sndFile.Instruments[ins]; if(undo.editedEnvelope < ENV_MAXTYPES) { instr->GetEnvelope(undo.editedEnvelope) = undo.instr.GetEnvelope(undo.editedEnvelope); } else { *instr = undo.instr; } DeleteStep(fromBuf, ins, fromBuf[ins - 1].size() - 1); modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); modDoc.SetModified(); return true; } // Delete a given undo / redo step of a Instrument. void CInstrumentUndo::DeleteStep(undobuf_t &buffer, const INSTRUMENTINDEX ins, const size_t step) { if(!InstrumentBufferExists(buffer, ins) || step >= buffer[ins - 1].size()) return; buffer[ins - 1].erase(buffer[ins - 1].begin() + step); } // Public helper function to remove the most recent undo point. void CInstrumentUndo::RemoveLastUndoStep(const INSTRUMENTINDEX ins) { if(!CanUndo(ins)) return; DeleteStep(UndoBuffer, ins, UndoBuffer[ins - 1].size() - 1); } // Update undo buffer when using rearrange instruments functionality. // newIndex contains one new index for each old index. newIndex[1] represents the first instrument. void CInstrumentUndo::RearrangeInstruments(undobuf_t &buffer, const std::vector &newIndex) { undobuf_t newBuf(modDoc.GetNumInstruments()); const INSTRUMENTINDEX newSize = static_cast(newIndex.size()); const INSTRUMENTINDEX oldSize = static_cast(buffer.size()); for(INSTRUMENTINDEX ins = 1; ins <= oldSize; ins++) { MPT_ASSERT(ins >= newSize || newIndex[ins] <= modDoc.GetNumInstruments()); if(ins < newSize && newIndex[ins] > 0 && newIndex[ins] <= modDoc.GetNumInstruments()) { newBuf[newIndex[ins] - 1] = buffer[ins - 1]; } else { ClearUndo(ins); } } #ifdef _DEBUG for(size_t i = 0; i < oldSize; i++) { if(i + 1 < newIndex.size() && newIndex[i + 1] != 0) MPT_ASSERT(newBuf[newIndex[i + 1] - 1].size() == buffer[i].size()); else MPT_ASSERT(buffer[i].empty()); } #endif buffer = newBuf; } // Update undo buffer when using rearrange samples functionality. // newIndex contains one new index for each old index. newIndex[1] represents the first sample. void CInstrumentUndo::RearrangeSamples(undobuf_t &buffer, const INSTRUMENTINDEX ins, std::vector &newIndex) { const CSoundFile &sndFile = modDoc.GetSoundFile(); if(sndFile.Instruments[ins] == nullptr || !InstrumentBufferExists(buffer, ins) || buffer[ins - 1].empty()) return; for(auto &i : buffer[ins - 1]) if(i.editedEnvelope >= ENV_MAXTYPES) { for(auto &sample : i.instr.Keyboard) { if(sample < newIndex.size()) sample = newIndex[sample]; else sample = 0; } } } // Ensure that the undo buffer is big enough for a given Instrument number bool CInstrumentUndo::InstrumentBufferExists(const undobuf_t &buffer, const INSTRUMENTINDEX ins) const { if(ins == 0 || ins >= MAX_INSTRUMENTS) return false; if(ins <= buffer.size()) return true; return false; } // Get name of next undo item const char *CInstrumentUndo::GetUndoName(const INSTRUMENTINDEX ins) const { if(!CanUndo(ins)) { return ""; } return UndoBuffer[ins - 1].back().description; } // Get name of next redo item const char *CInstrumentUndo::GetRedoName(const INSTRUMENTINDEX ins) const { if(!CanRedo(ins)) { return ""; } return RedoBuffer[ins - 1].back().description; } OPENMPT_NAMESPACE_END