diff --git a/src/surge-xt/CMakeLists.txt b/src/surge-xt/CMakeLists.txt index 4f2b5da5ad0..49c7e34fb0b 100644 --- a/src/surge-xt/CMakeLists.txt +++ b/src/surge-xt/CMakeLists.txt @@ -95,6 +95,7 @@ target_sources(${PROJECT_NAME} PRIVATE gui/SurgeJUCEHelpers.h gui/SurgeJUCELookAndFeel.cpp gui/SurgeJUCELookAndFeel.h + gui/UndoManager.cpp gui/overlays/AboutScreen.cpp gui/overlays/AboutScreen.h gui/overlays/CoveringMessageOverlay.cpp diff --git a/src/surge-xt/SurgeSynthProcessor.cpp b/src/surge-xt/SurgeSynthProcessor.cpp index cd2d30e6761..bdd19569e04 100644 --- a/src/surge-xt/SurgeSynthProcessor.cpp +++ b/src/surge-xt/SurgeSynthProcessor.cpp @@ -15,6 +15,11 @@ #include "version.h" #include "sst/plugininfra/cpufeatures.h" +/* + * This is a bit odd but - this is an editor concept with the lifetime of the processor + */ +#include "gui/UndoManager.h" + //============================================================================== SurgeSynthProcessor::SurgeSynthProcessor() : juce::AudioProcessor(BusesProperties() @@ -429,7 +434,9 @@ void SurgeSynthProcessor::surgeParameterUpdated(const SurgeSynthesizer::ID &id, { auto spar = paramsByID[id]; if (spar) + { spar->setValueNotifyingHost(f); + } } void SurgeSynthProcessor::surgeMacroUpdated(const long id, float f) diff --git a/src/surge-xt/SurgeSynthProcessor.h b/src/surge-xt/SurgeSynthProcessor.h index 10ea2f3196c..409ad44bf4e 100644 --- a/src/surge-xt/SurgeSynthProcessor.h +++ b/src/surge-xt/SurgeSynthProcessor.h @@ -26,6 +26,14 @@ #include #endif +namespace Surge +{ +namespace GUI +{ +struct UndoManager; +} +} // namespace Surge + //============================================================================== /** */ @@ -247,5 +255,9 @@ class SurgeSynthProcessor : public juce::AudioProcessor, int checkNamesEvery = 0; + public: + std::unique_ptr undoManager; + + private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SurgeSynthProcessor) }; diff --git a/src/surge-xt/gui/SurgeGUIEditor.cpp b/src/surge-xt/gui/SurgeGUIEditor.cpp index 44da903ac44..8969d4d7101 100644 --- a/src/surge-xt/gui/SurgeGUIEditor.cpp +++ b/src/surge-xt/gui/SurgeGUIEditor.cpp @@ -418,6 +418,11 @@ SurgeGUIEditor::SurgeGUIEditor(SurgeSynthEditor *jEd, SurgeSynthesizer *synth) juce::Desktop::getInstance().addFocusChangeListener(this); setupKeymapManager(); + + if (!juceEditor->processor.undoManager) + juceEditor->processor.undoManager = + std::make_unique(this, this->synth); + juceEditor->processor.undoManager->resetEditor(this); } SurgeGUIEditor::~SurgeGUIEditor() @@ -2143,6 +2148,20 @@ void SurgeGUIEditor::controlBeginEdit(Surge::GUI::IComponentTagValue *control) int ptag = tag - start_paramtags; if (ptag >= 0 && ptag < synth->storage.getPatch().param_ptr.size()) { + if (mod_editor) + { + auto mci = dynamic_cast(control); + if (mci) + { + undoManager()->pushModulationChange(ptag, modsource, current_scene, modsource_index, + mci->modValue); + } + } + else + { + undoManager()->pushParameterChange(ptag, + synth->storage.getPatch().param_ptr[ptag]->val); + } juceEditor->beginParameterEdit(synth->storage.getPatch().param_ptr[ptag]); } else if (tag_mod_source0 + ms_ctrl1 <= tag && @@ -2177,6 +2196,30 @@ void SurgeGUIEditor::controlEndEdit(Surge::GUI::IComponentTagValue *control) } } +const std::unique_ptr &SurgeGUIEditor::undoManager() +{ + return juceEditor->processor.undoManager; +} + +void SurgeGUIEditor::setParamFromUndo(int paramId, pdata val) +{ + auto p = synth->storage.getPatch().param_ptr[paramId]; + auto id = synth->idForParameter(p); + juceEditor->beginParameterEdit(p); + p->val = val; + synth->sendParameterAutomation(id, synth->getParameter01(id)); + juceEditor->endParameterEdit(p); + synth->refresh_editor = true; +} + +void SurgeGUIEditor::setModulationFromUndo(int paramId, modsources ms, int scene, int idx, + float val) +{ + auto p = synth->storage.getPatch().param_ptr[paramId]; + // FIXME scene and index + synth->setModulation(p->id, ms, scene, idx, val); + synth->refresh_editor = true; +} //------------------------------------------------------------------------------------------------ long SurgeGUIEditor::applyParameterOffset(long id) { return id - start_paramtags; } @@ -2422,6 +2465,8 @@ void SurgeGUIEditor::showSettingsMenu(const juce::Point &where, { auto settingsMenu = juce::PopupMenu(); + // settingsMenu.addItem("HACK DUMP", [this]() { undoManager()->dumpStack(); }); + auto zoomMenu = makeZoomMenu(where, false); settingsMenu.addSubMenu("Zoom", zoomMenu); @@ -5942,6 +5987,7 @@ void SurgeGUIEditor::broadcastMSEGState() void SurgeGUIEditor::repushAutomationFor(Parameter *p) { auto id = synth->idForParameter(p); + synth->sendParameterAutomation(id, synth->getParameter01(id)); } @@ -6215,6 +6261,7 @@ void SurgeGUIEditor::setupKeymapManager() Surge::GUI::keyboardActionName, [](auto a, auto b) {}); keyMapManager->clearBindings(); + keyMapManager->addBinding(Surge::GUI::UNDO, {keymap_t::Modifiers::CONTROL, (int)'Z'}); keyMapManager->addBinding(Surge::GUI::OSC_1, {keymap_t::Modifiers::ALT, (int)'1'}); keyMapManager->addBinding(Surge::GUI::OSC_2, {keymap_t::Modifiers::ALT, (int)'2'}); keyMapManager->addBinding(Surge::GUI::OSC_3, {keymap_t::Modifiers::ALT, (int)'3'}); @@ -6312,6 +6359,9 @@ bool SurgeGUIEditor::keyPressed(const juce::KeyPress &key, juce::Component *orig switch (action) { + case Surge::GUI::UNDO: + undoManager()->undo(); + return true; case Surge::GUI::OSC_1: changeSelectedOsc(0); return true; diff --git a/src/surge-xt/gui/SurgeGUIEditor.h b/src/surge-xt/gui/SurgeGUIEditor.h index c80f37e1004..ae23fbe89d6 100644 --- a/src/surge-xt/gui/SurgeGUIEditor.h +++ b/src/surge-xt/gui/SurgeGUIEditor.h @@ -42,6 +42,7 @@ #include #include #include +#include "UndoManager.h" class SurgeSynthEditor; @@ -470,6 +471,10 @@ class SurgeGUIEditor : public Surge::GUI::IComponentTagValue::Listener, bool canDropTarget(const std::string &fname); // these come as const char* from vstgui bool onDrop(const std::string &fname); + const std::unique_ptr &undoManager(); + void setParamFromUndo(int paramId, pdata val); + void setModulationFromUndo(int paramId, modsources ms, int scene, int idx, float val); + private: juce::Rectangle positionForModulationGrid(modsources entry); juce::Rectangle positionForModOverview(); diff --git a/src/surge-xt/gui/SurgeGUIEditorKeyboardActions.h b/src/surge-xt/gui/SurgeGUIEditorKeyboardActions.h index 8f5c60a5b75..bab2cfcc5b0 100644 --- a/src/surge-xt/gui/SurgeGUIEditorKeyboardActions.h +++ b/src/surge-xt/gui/SurgeGUIEditorKeyboardActions.h @@ -24,6 +24,8 @@ namespace GUI { enum KeyboardActions { + UNDO, + SAVE_PATCH, FIND_PATCH, FAVORITE_PATCH, @@ -61,6 +63,9 @@ inline std::string keyboardActionName(KeyboardActions a) { switch (a) { + case UNDO: + return "UNDO"; + case OSC_1: return "OSC_1"; @@ -127,6 +132,8 @@ inline std::string keyboardActionDescription(KeyboardActions a) { switch (a) { + case UNDO: + return "Undo changes"; case SAVE_PATCH: return "Open Save Patch Dialog"; case FIND_PATCH: diff --git a/src/surge-xt/gui/SurgeGUIEditorValueCallbacks.cpp b/src/surge-xt/gui/SurgeGUIEditorValueCallbacks.cpp index f1b39a2b934..92bb9f6e55d 100644 --- a/src/surge-xt/gui/SurgeGUIEditorValueCallbacks.cpp +++ b/src/surge-xt/gui/SurgeGUIEditorValueCallbacks.cpp @@ -1498,6 +1498,7 @@ int32_t SurgeGUIEditor::controlModifierClicked(Surge::GUI::IComponentTagValue *c addTo->addItem(displaytxt, true, isChecked, [this, p, i, tag]() { float ef = Parameter::intScaledToFloat(i, p->val_max.i, p->val_min.i); + undoManager()->pushParameterChange(p->id, p->val); synth->setParameter01(synth->idForParameter(p), ef, false, false); @@ -1505,7 +1506,7 @@ int32_t SurgeGUIEditor::controlModifierClicked(Surge::GUI::IComponentTagValue *c { updateWaveshaperOverlay(); } - repushAutomationFor(p); + broadcastPluginAutomationChangeFor(p); synth->refresh_editor = true; }); } @@ -1526,13 +1527,14 @@ int32_t SurgeGUIEditor::controlModifierClicked(Surge::GUI::IComponentTagValue *c std::string displaytxt = txt; - contextMenu.addItem(displaytxt, true, (i == p->val.i), - [this, ef, p, i]() { - synth->setParameter01(synth->idForParameter(p), - ef, false, false); - repushAutomationFor(p); - synth->refresh_editor = true; - }); + contextMenu.addItem( + displaytxt, true, (i == p->val.i), [this, ef, p, i]() { + undoManager()->pushParameterChange(p->id, p->val); + synth->setParameter01(synth->idForParameter(p), ef, false, + false); + broadcastPluginAutomationChangeFor(p); + synth->refresh_editor = true; + }); } if (isCombOnSubtype) diff --git a/src/surge-xt/gui/UndoManager.cpp b/src/surge-xt/gui/UndoManager.cpp new file mode 100644 index 00000000000..1a3f27d24d5 --- /dev/null +++ b/src/surge-xt/gui/UndoManager.cpp @@ -0,0 +1,295 @@ +/* +** Surge Synthesizer is Free and Open Source Software +** +** Surge is made available under the Gnu General Public License, v3.0 +** https://www.gnu.org/licenses/gpl-3.0.en.html +** +** Copyright 2004-2022 by various individuals as described by the Git transaction log +** +** All source at: https://github.com/surge-synthesizer/surge.git +** +** Surge was a commercial product from 2004-2018, with Copyright and ownership +** in that period held by Claes Johanson at Vember Audio. Claes made Surge +** open source in September 2018. +*/ + +#include "UndoManager.h" +#include "SurgeGUIEditor.h" +#include "SurgeSynthesizer.h" +#include +#include +#include + +namespace Surge +{ +namespace GUI +{ +struct UndoManagerImpl +{ + static constexpr int max_stack = 250; + SurgeGUIEditor *editor; + SurgeSynthesizer *synth; + UndoManagerImpl(SurgeGUIEditor *ed, SurgeSynthesizer *s) : editor(ed), synth(s) {} + + // for now this is super simple + struct UndoParam + { + int paramId; + pdata val; + }; + struct UndoModulation + { + int paramId; + float val; + int scene; + int index; + modsources ms; + }; + struct UndoOscillator + { + int oscNum; + int scene; + int type; + std::vector> paramIdValues; + }; + struct UndoFX + { + int fxslot; + int type; + std::vector> paramIdValues; + }; + + // If you add a new type here add it both to aboutTheSameThing, toString, and + // to undo. + typedef std::variant UndoAction; + struct UndoRecord + { + UndoAction action; + std::chrono::time_point time; + UndoRecord(const UndoAction &a) : action(a) + { + time = std::chrono::high_resolution_clock::now(); + } + }; + std::deque undoStack; + + /* Not same value, but same pair. Used for wheel event compressing for instance */ + bool aboutTheSameThing(const UndoAction &a, const UndoAction &b) + { + if (auto pa = std::get_if(&a)) + { + auto pb = std::get_if(&b); + return pa->paramId == pb->paramId; + } + if (auto pa = std::get_if(&a)) + { + auto pb = std::get_if(&b); + return (pa->paramId == pb->paramId) && (pa->scene == pb->scene) && (pa->ms == pb->ms) && + (pa->index == pb->index); + } + + return false; + } + + std::string toString(const UndoAction &a) + { + if (auto pa = std::get_if(&a)) + { + return "PARAM"; + } + if (auto pa = std::get_if(&a)) + { + return "MOD"; + } + + return "UNK"; + } + + void push(const UndoAction &r) + { + if (undoStack.empty()) + { + undoStack.emplace_back(r); + return; + } + + auto &t = undoStack.back(); + if (r.index() != t.action.index()) + { + undoStack.emplace_back(r); + } + else + { + auto n = std::chrono::high_resolution_clock::now(); + auto d = std::chrono::duration_cast(n - t.time); + + if (d.count() < 200 && aboutTheSameThing(r, t.action)) + { + t.time = n; + } + else + { + undoStack.emplace_back(r); + } + } + while (undoStack.size() > max_stack) + { + undoStack.pop_front(); + } + } + + void pushParameterChange(int paramId, pdata val) + { + auto r = UndoParam(); + r.paramId = paramId; + r.val = val; + push(r); + } + void pushModulationChange(int paramId, modsources modsource, int sc, int idx, float val) + { + auto r = UndoModulation(); + r.paramId = paramId; + r.val = val; + r.ms = modsource; + r.scene = sc; + r.index = idx; + + push(r); + } + + void pushOscillator(int scene, int oscnum) + { + auto os = &(synth->storage.getPatch().scene[scene].osc[oscnum]); + auto r = UndoOscillator(); + r.scene = scene; + r.oscNum = oscnum; + r.type = os->type.val.i; + + Parameter *p = &(os->type); + p++; + while (p <= &(os->retrigger)) + { + r.paramIdValues.emplace_back(p->id, p->val); + p++; + } + + push(r); + } + + void pushFX(int fxslot) + { + auto fx = &(synth->storage.getPatch().fx[fxslot]); + auto r = UndoFX(); + r.fxslot = fxslot; + r.type = fx->type.val.i; + + for (int i = 0; i < n_fx_params; ++i) + { + r.paramIdValues.emplace_back(fx->p[i].id, fx->p[i].val); + } + + push(r); + } + + bool undo() + { + if (undoStack.empty()) + return false; + + auto qt = undoStack.back(); + auto q = qt.action; + undoStack.pop_back(); + + // this would be cleaner with std:visit but visit isn't in macos libc until 10.13 + if (auto p = std::get_if(&q)) + { + editor->setParamFromUndo(p->paramId, p->val); + return true; + } + if (auto p = std::get_if(&q)) + { + editor->setModulationFromUndo(p->paramId, p->ms, p->scene, p->index, p->val); + return true; + } + if (auto p = std::get_if(&q)) + { + auto os = &(synth->storage.getPatch().scene[p->scene].osc[p->oscNum]); + os->type.val.i = p->type; + synth->storage.getPatch().update_controls(false, os, false); + + for (auto q : p->paramIdValues) + { + editor->setParamFromUndo(q.first, q.second); + } + return true; + } + if (auto p = std::get_if(&q)) + { + std::lock_guard g(synth->fxSpawnMutex); + + int cge = p->fxslot; + + synth->fxsync[cge].type.val.i = p->type; + Effect *t_fx = spawn_effect(synth->fxsync[cge].type.val.i, &synth->storage, + &synth->fxsync[cge], 0); + if (t_fx) + { + t_fx->init_ctrltypes(); + t_fx->init_default_values(); + delete t_fx; + } + + synth->switch_toggled_queued = true; + synth->load_fx_needed = true; + synth->fx_reload[cge] = true; + for (int i = 0; i < n_fx_params; ++i) + { + synth->fxsync[cge].p[i].val = p->paramIdValues[i].second; + } + return true; + } + + return false; + } + + void dumpStack() + { + for (const auto &q : undoStack) + { + std::cout << toString(q.action) << " " << q.time.time_since_epoch().count() << " " + << q.action.index() << std::endl; + } + } +}; + +UndoManager::UndoManager(SurgeGUIEditor *ed, SurgeSynthesizer *synth) +{ + impl = std::make_unique(ed, synth); +} + +UndoManager::~UndoManager() = default; + +void UndoManager::pushParameterChange(int paramId, pdata val) +{ + impl->pushParameterChange(paramId, val); +} + +void UndoManager::pushModulationChange(int paramId, modsources modsource, int scene, int idx, + float val) +{ + impl->pushModulationChange(paramId, modsource, scene, idx, val); +} + +void UndoManager::pushOscillator(int scene, int oscnum) { impl->pushOscillator(scene, oscnum); } + +void UndoManager::pushFX(int fxslot) { impl->pushFX(fxslot); } + +bool UndoManager::undo() { return impl->undo(); } + +void UndoManager::dumpStack() { impl->dumpStack(); } + +void UndoManager::resetEditor(SurgeGUIEditor *ed) { impl->editor = ed; } + +} // namespace GUI + +} // namespace Surge \ No newline at end of file diff --git a/src/surge-xt/gui/UndoManager.h b/src/surge-xt/gui/UndoManager.h new file mode 100644 index 00000000000..2ea66100973 --- /dev/null +++ b/src/surge-xt/gui/UndoManager.h @@ -0,0 +1,49 @@ +/* +** Surge Synthesizer is Free and Open Source Software +** +** Surge is made available under the Gnu General Public License, v3.0 +** https://www.gnu.org/licenses/gpl-3.0.en.html +** +** Copyright 2004-2022 by various individuals as described by the Git transaction log +** +** All source at: https://github.com/surge-synthesizer/surge.git +** +** Surge was a commercial product from 2004-2018, with Copyright and ownership +** in that period held by Claes Johanson at Vember Audio. Claes made Surge +** open source in September 2018. +*/ + +#ifndef SURGE_UNDOMANAGER_H +#define SURGE_UNDOMANAGER_H + +#include "Parameter.h" +#include "ModulationSource.h" + +struct SurgeSynthesizer; +struct SurgeGUIEditor; + +namespace Surge +{ +namespace GUI +{ +struct UndoManagerImpl; +struct UndoManager +{ + UndoManager(SurgeGUIEditor *ed, SurgeSynthesizer *synth); + ~UndoManager(); + + void resetEditor(SurgeGUIEditor *ed); + + std::unique_ptr impl; + void pushParameterChange(int paramId, pdata val); + void pushMacroChange(int macroid, float val); + void pushModulationChange(int paramId, modsources modsource, int scene, int index, float val); + void pushOscillator(int scene, int oscnum); + void pushFX(int fxslot); + bool undo(); + void dumpStack(); +}; +} // namespace GUI +} // namespace Surge + +#endif // SURGE_UNDOMANAGER_H diff --git a/src/surge-xt/gui/widgets/MultiSwitch.cpp b/src/surge-xt/gui/widgets/MultiSwitch.cpp index fe7ba8a4b59..993b4f9a9a5 100644 --- a/src/surge-xt/gui/widgets/MultiSwitch.cpp +++ b/src/surge-xt/gui/widgets/MultiSwitch.cpp @@ -151,7 +151,7 @@ void MultiSwitch::mouseDown(const juce::MouseEvent &event) mouseDownLongHold(event); setValue(coordinateToValue(event.x, event.y)); - notifyValueChanged(); + notifyValueChangedWithBeginEnd(); if (isHovered) { diff --git a/src/surge-xt/gui/widgets/Switch.cpp b/src/surge-xt/gui/widgets/Switch.cpp index e23088ff0da..0a5b21f8e6b 100644 --- a/src/surge-xt/gui/widgets/Switch.cpp +++ b/src/surge-xt/gui/widgets/Switch.cpp @@ -87,7 +87,7 @@ void Switch::mouseDown(const juce::MouseEvent &event) setValueDirection(1); } - notifyValueChanged(); + notifyValueChangedWithBeginEnd(); } else { @@ -95,7 +95,7 @@ void Switch::mouseDown(const juce::MouseEvent &event) { value = (value > 0.5) ? 0 : 1; - notifyValueChanged(); + notifyValueChangedWithBeginEnd(); } } } @@ -114,7 +114,7 @@ void Switch::mouseWheelMove(const juce::MouseEvent &event, const juce::MouseWhee { storage->getPatch().isDirty = true; setValueDirection(mul); - notifyValueChanged(); + notifyValueChangedWithBeginEnd(); } else { @@ -123,7 +123,7 @@ void Switch::mouseWheelMove(const juce::MouseEvent &event, const juce::MouseWhee if (ov != value) { - notifyValueChanged(); + notifyValueChangedWithBeginEnd(); } } } @@ -219,7 +219,7 @@ struct SwitchAH : public juce::AccessibilityHandler if (mswitch->isMultiIntegerValued()) { mswitch->setValueDirection(1); - mswitch->notifyValueChanged(); + mswitch->notifyValueChangedWithBeginEnd(); } else { @@ -227,7 +227,7 @@ struct SwitchAH : public juce::AccessibilityHandler { auto value = (mswitch->getValue() > 0.5) ? 0 : 1; mswitch->setValue(value); - mswitch->notifyValueChanged(); + mswitch->notifyValueChangedWithBeginEnd(); } } } diff --git a/src/surge-xt/gui/widgets/WidgetBaseMixin.h b/src/surge-xt/gui/widgets/WidgetBaseMixin.h index 288ca93a104..14973356745 100644 --- a/src/surge-xt/gui/widgets/WidgetBaseMixin.h +++ b/src/surge-xt/gui/widgets/WidgetBaseMixin.h @@ -87,6 +87,13 @@ struct WidgetBaseMixin : public Surge::GUI::SkinConsumingComponent, t->controlEndEdit(this); } + void notifyValueChangedWithBeginEnd() + { + notifyBeginEdit(); + notifyValueChanged(); + notifyEndEdit(); + } + virtual void updateAccessibleStateOnUserValueChange() {} juce::Point enqueueStartPosition{-18.f, -18.f}; diff --git a/src/surge-xt/gui/widgets/XMLConfiguredMenus.cpp b/src/surge-xt/gui/widgets/XMLConfiguredMenus.cpp index d2ea12a5535..49d6c8282fe 100644 --- a/src/surge-xt/gui/widgets/XMLConfiguredMenus.cpp +++ b/src/surge-xt/gui/widgets/XMLConfiguredMenus.cpp @@ -360,6 +360,7 @@ void OscillatorMenu::loadSnapshot(int type, TiXmlElement *e, int idx) { auto sc = sge->current_scene; sge->oscilatorMenuIndex[sc][sge->current_osc[sc]] = idx; + sge->undoManager()->pushOscillator(sc, sge->current_osc[sc]); } osc->queue_type = type; osc->queue_xmldata = e; @@ -538,6 +539,11 @@ void FxMenu::mouseExit(const juce::MouseEvent &event) void FxMenu::loadSnapshot(int type, TiXmlElement *e, int idx) { + auto sge = firstListenerOfType(); + if (sge) + { + sge->undoManager()->pushFX(current_fx); + } if (type > -1) { fxbuffer->type.val.i = type; @@ -716,6 +722,12 @@ void FxMenu::saveFX() void FxMenu::loadUserPreset(const Surge::Storage::FxUserPreset::Preset &p) { + auto sge = firstListenerOfType(); + if (sge) + { + sge->undoManager()->pushFX(current_fx); + } + this->storage->fxUserPreset->loadPresetOnto(p, storage, fxbuffer); selectedIdx = -1;