1 #include "globals.h"
2 #include "SurgeGUIEditor.h"
3 #include "DspUtilities.h"
4 #include "CSnapshotMenu.h"
5 #include "effect/Effect.h"
6 #include "CScalableBitmap.h"
7 #include "SurgeBitmaps.h"
8 #include "SurgeStorage.h"
9 #include "filesystem/import.h"
10 #include "SkinColors.h"
11 #include "RuntimeFont.h"
12 #include "guihelpers.h"
13 #include "StringOps.h"
14 
15 #include <iostream>
16 #include <iomanip>
17 #include <queue>
18 
19 using namespace VSTGUI;
20 
21 extern CFontRef displayFont;
22 
23 // CSnapshotMenu
24 
CSnapshotMenu(const CRect & size,IControlListener * listener,long tag,SurgeStorage * storage)25 CSnapshotMenu::CSnapshotMenu(const CRect &size, IControlListener *listener, long tag,
26                              SurgeStorage *storage)
27     : COptionMenu(size, nullptr, tag, 0)
28 {
29     this->storage = storage;
30     this->listenerNotForParent = listener;
31 }
~CSnapshotMenu()32 CSnapshotMenu::~CSnapshotMenu() {}
33 
onMouseDown(CPoint & where,const CButtonState & button)34 CMouseEventResult CSnapshotMenu::onMouseDown(CPoint &where, const CButtonState &button)
35 {
36     if (listenerNotForParent && (button & (kMButton | kButton4 | kButton5)))
37     {
38         listenerNotForParent->controlModifierClicked(this, button);
39         return kMouseDownEventHandledButDontNeedMovedOrUpEvents;
40     }
41     return COptionMenu::onMouseDown(where, button);
42 }
43 
draw(CDrawContext * dc)44 void CSnapshotMenu::draw(CDrawContext *dc) { setDirty(false); }
45 
canSave()46 bool CSnapshotMenu::canSave() { return false; }
47 
populate()48 void CSnapshotMenu::populate()
49 {
50     int main = 0, sub = 0;
51     const long max_main = 128, max_sub = 256;
52 
53     int idx = 0;
54     TiXmlElement *sect = storage->getSnapshotSection(mtype);
55     if (sect)
56     {
57         TiXmlElement *type = sect->FirstChildElement();
58 
59         while (type)
60         {
61             if (type->Value() && strcmp(type->Value(), "type") == 0)
62             {
63                 auto sm = populateSubmenuFromTypeElement(type, this, main, sub, max_sub, idx);
64 
65                 if (sm)
66                 {
67                     addToTopLevelTypeMenu(type, sm, idx);
68                 }
69             }
70             else if (type->Value() && strcmp(type->Value(), "separator") == 0)
71             {
72                 addSeparator();
73             }
74 
75             type = type->NextSiblingElement();
76             main++;
77 
78             if (main >= max_main)
79             {
80                 break;
81             }
82         }
83     }
84     maxIdx = idx;
85 }
86 
loadSnapshotByIndex(int idx)87 bool CSnapshotMenu::loadSnapshotByIndex(int idx)
88 {
89     int cidx = 0;
90     // This isn't that efficient but you know
91     TiXmlElement *sect = storage->getSnapshotSection(mtype);
92     if (sect)
93     {
94         std::queue<TiXmlElement *> typeD;
95         typeD.push(TINYXML_SAFE_TO_ELEMENT(sect->FirstChild("type")));
96         while (!typeD.empty())
97         {
98             auto type = typeD.front();
99             typeD.pop();
100             int type_id = 0;
101             type->Attribute("i", &type_id);
102             TiXmlElement *snapshot = TINYXML_SAFE_TO_ELEMENT(type->FirstChild("snapshot"));
103             while (snapshot)
104             {
105                 int snapshotTypeID = type_id, tmpI = 0;
106                 if (snapshot->Attribute("i", &tmpI) != nullptr)
107                 {
108                     snapshotTypeID = tmpI;
109                 }
110 
111                 if (cidx == idx)
112                 {
113                     selectedIdx = cidx;
114                     loadSnapshot(snapshotTypeID, snapshot, cidx);
115                     if (listenerNotForParent)
116                         listenerNotForParent->valueChanged(this);
117                     return true;
118                 }
119                 snapshot = TINYXML_SAFE_TO_ELEMENT(snapshot->NextSibling("snapshot"));
120                 cidx++;
121             }
122 
123             auto subType = TINYXML_SAFE_TO_ELEMENT(type->FirstChild("type"));
124             while (subType)
125             {
126                 typeD.push(subType);
127                 subType = TINYXML_SAFE_TO_ELEMENT(subType->NextSibling("type"));
128             }
129 
130             auto next = TINYXML_SAFE_TO_ELEMENT(type->NextSibling("type"));
131             if (next)
132                 typeD.push(next);
133         }
134     }
135     if (idx < 0 && cidx + idx > 0)
136     {
137         // I've gone off the end
138         return (loadSnapshotByIndex(cidx + idx));
139     }
140     return false;
141 }
142 
populateSubmenuFromTypeElement(TiXmlElement * type,VSTGUI::COptionMenu * parent,int & main,int & sub,const long & max_sub,int & idx)143 VSTGUI::COptionMenu *CSnapshotMenu::populateSubmenuFromTypeElement(TiXmlElement *type,
144                                                                    VSTGUI::COptionMenu *parent,
145                                                                    int &main, int &sub,
146                                                                    const long &max_sub, int &idx)
147 {
148     /*
149     ** Begin by grabbing all the snapshot elements
150     */
151     std::string txt;
152     int type_id = 0;
153     type->Attribute("i", &type_id);
154     sub = 0;
155     COptionMenu *subMenu = new COptionMenu(getViewSize(), 0, main, 0, 0, kNoDrawStyle);
156     subMenu->setNbItemsPerColumn(20);
157     TiXmlElement *snapshot = TINYXML_SAFE_TO_ELEMENT(type->FirstChild("snapshot"));
158     while (snapshot)
159     {
160         txt = snapshot->Attribute("name");
161 
162 #if WINDOWS
163         Surge::Storage::findReplaceSubstring(txt, std::string("&"), std::string("&&"));
164 #endif
165 
166         int snapshotTypeID = type_id;
167         int tmpI;
168         if (snapshot->Attribute("i", &tmpI) != nullptr)
169         {
170             snapshotTypeID = tmpI;
171         }
172 
173         if (firstSnapshotByType.find(snapshotTypeID) == firstSnapshotByType.end())
174             firstSnapshotByType[snapshotTypeID] = idx;
175         auto actionItem = new CCommandMenuItem(CCommandMenuItem::Desc(txt.c_str()));
176         auto action = [this, snapshot, snapshotTypeID, idx](CCommandMenuItem *item) {
177             this->selectedIdx = idx;
178             this->loadSnapshot(snapshotTypeID, snapshot, idx);
179             if (this->listenerNotForParent)
180                 this->listenerNotForParent->valueChanged(this);
181         };
182         loadArgsByIndex.push_back(std::make_pair(snapshotTypeID, snapshot));
183         idx++;
184 
185         actionItem->setActions(action, nullptr);
186         subMenu->addEntry(actionItem);
187 
188         snapshot = TINYXML_SAFE_TO_ELEMENT(snapshot->NextSibling("snapshot"));
189         sub++;
190         if (sub >= max_sub)
191             break;
192     }
193 
194     /*
195     ** Next see if we have any subordinate types
196     */
197     TiXmlElement *subType = TINYXML_SAFE_TO_ELEMENT(type->FirstChild("type"));
198     while (subType)
199     {
200         populateSubmenuFromTypeElement(subType, subMenu, main, sub, max_sub, idx);
201         subType = TINYXML_SAFE_TO_ELEMENT(subType->NextSibling("type"));
202     }
203 
204     /*
205     ** Then add myself to parent
206     */
207     txt = type->Attribute("name");
208 
209 #if WINDOWS
210     Surge::Storage::findReplaceSubstring(txt, std::string("&"), std::string("&&"));
211 #endif
212 
213     if (sub)
214     {
215         parent->addEntry(subMenu, txt.c_str());
216     }
217     else
218     {
219         auto actionItem = new CCommandMenuItem(CCommandMenuItem::Desc(txt.c_str()));
220 
221         if (firstSnapshotByType.find(type_id) == firstSnapshotByType.end())
222             firstSnapshotByType[type_id] = idx;
223 
224         auto action = [this, type_id, type, idx](CCommandMenuItem *item) {
225             this->selectedIdx = 0;
226             this->loadSnapshot(type_id, type, idx);
227             if (this->listenerNotForParent)
228                 this->listenerNotForParent->valueChanged(this);
229         };
230 
231         loadArgsByIndex.push_back(std::make_pair(type_id, type));
232         idx++;
233         actionItem->setActions(action, nullptr);
234         parent->addEntry(actionItem);
235     }
236 
237     subMenu->forget();
238     if (sub)
239         return subMenu; // OK to return forgoten since it has lifetime of parent
240     else
241         return nullptr;
242 }
243 
244 // COscMenu
245 
COscMenu(const CRect & size,IControlListener * listener,long tag,SurgeStorage * storage,OscillatorStorage * osc,std::shared_ptr<SurgeBitmaps> bitmapStore)246 COscMenu::COscMenu(const CRect &size, IControlListener *listener, long tag, SurgeStorage *storage,
247                    OscillatorStorage *osc, std::shared_ptr<SurgeBitmaps> bitmapStore)
248     : CSnapshotMenu(size, listener, tag, storage)
249 {
250     strcpy(mtype, "osc");
251     this->osc = osc;
252     this->storage = storage;
253     auto tb = bitmapStore->getBitmap(IDB_OSC_MENU);
254     bmp = tb;
255     populate();
256 }
257 
draw(CDrawContext * dc)258 void COscMenu::draw(CDrawContext *dc)
259 {
260     if (!attemptedHoverLoad)
261     {
262         hoverBmp = skin->hoverBitmapOverlayForBackgroundBitmap(
263             skinControl, dynamic_cast<CScalableBitmap *>(bmp), associatedBitmapStore,
264             Surge::UI::Skin::HOVER);
265         attemptedHoverLoad = true;
266     }
267     CRect size = getViewSize();
268     int i = osc->type.val.i;
269     int y = i * size.getHeight();
270 
271     // we're using code generated text for this menu from skin version 2 onwards
272     // so there's no need to slice the graphics asset
273     if (skin->getVersion() >= 2)
274     {
275         y = 0;
276     }
277 
278     if (bmp)
279         bmp->draw(dc, size, CPoint(0, y), 0xff);
280 
281     if (isHovered && hoverBmp)
282         hoverBmp->draw(dc, size, CPoint(0, y), 0xff);
283 
284     if (skin->getVersion() >= 2)
285     {
286         dc->saveGlobalState();
287 
288         dc->setDrawMode(VSTGUI::kAntiAliasing | VSTGUI::kNonIntegralMode);
289         dc->setFont(Surge::GUI::getLatoAtSize(font_size, font_style));
290 
291         if (isHovered)
292         {
293             dc->setFontColor(skin->getColor(Colors::Osc::Type::TextHover));
294         }
295         else
296         {
297             dc->setFontColor(skin->getColor(Colors::Osc::Type::Text));
298         }
299 
300         std::string txt = osc_type_shortnames[i];
301 
302         if (text_allcaps)
303         {
304             std::transform(txt.begin(), txt.end(), txt.begin(), ::toupper);
305         }
306 
307         dc->drawString(txt.c_str(), size.offset(text_hoffset, text_voffset), text_align, true);
308 
309         dc->restoreGlobalState();
310     }
311 
312     setDirty(false);
313 }
314 
loadSnapshot(int type,TiXmlElement * e,int idx)315 void COscMenu::loadSnapshot(int type, TiXmlElement *e, int idx)
316 {
317     assert(within_range(0, type, n_osc_types));
318     auto sge = dynamic_cast<SurgeGUIEditor *>(listenerNotForParent);
319     if (sge)
320     {
321         auto sc = sge->current_scene;
322         sge->oscilatorMenuIndex[sc][sge->current_osc[sc]] = idx;
323     }
324     osc->queue_type = type;
325     osc->queue_xmldata = e;
326 }
327 
onWheel(const VSTGUI::CPoint & where,const float & distance,const VSTGUI::CButtonState & buttons)328 bool COscMenu::onWheel(const VSTGUI::CPoint &where, const float &distance,
329                        const VSTGUI::CButtonState &buttons)
330 {
331     accumWheel += distance;
332 #if WINDOWS // rough hack but it still takes too many mousewheel clicks to get outside of Classic
333             // osc subfolder onto other osc types!
334     accumWheel += distance;
335 #endif
336 
337     auto *sge = dynamic_cast<SurgeGUIEditor *>(listenerNotForParent);
338     if (sge)
339     {
340         auto sc = sge->current_scene;
341         int currentIdx = sge->oscilatorMenuIndex[sc][sge->current_osc[sc]];
342         if (accumWheel < -1)
343         {
344             currentIdx = currentIdx + 1;
345             if (currentIdx >= maxIdx)
346                 currentIdx = 0;
347             accumWheel = 0;
348             auto args = loadArgsByIndex[currentIdx];
349             loadSnapshot(args.first, args.second, currentIdx);
350         }
351         else if (accumWheel > 1)
352         {
353             currentIdx = currentIdx - 1;
354             if (currentIdx < 0)
355                 currentIdx = maxIdx - 1;
356             accumWheel = 0;
357             auto args = loadArgsByIndex[currentIdx];
358             loadSnapshot(args.first, args.second, currentIdx);
359         }
360     }
361     return true;
362 }
363 
364 /*void COscMenu::load_snapshot(int type, TiXmlElement *e)
365 {
366         assert(within_range(0,type,n_osc_types));
367         osc->type.val.i = type;
368         //osc->retrigger.val.i =
369         storage->patch.update_controls(false, osc);
370         if(e)
371         {
372                 for(int i=0; i<n_osc_params; i++)
373                 {
374                         double d; int j;
375                         char lbl[TXT_SIZE];
376                         snprintf(lbl, TXT_SIZE, "p%i",i);
377                         if (osc->p[i].valtype == vt_float)
378                         {
379                                 if(e->QueryDoubleAttribute(lbl,&d) == TIXML_SUCCESS) osc->p[i].val.f
380 = (float)d;
381                         }
382                         else
383                         {
384                                 if(e->QueryIntAttribute(lbl,&j) == TIXML_SUCCESS) osc->p[i].val.i =
385 j;
386                         }
387                 }
388         }
389 }*/
390 
391 // CFxMenu
392 
393 std::vector<float> CFxMenu::fxCopyPaste;
394 
CFxMenu(const CRect & size,IControlListener * listener,long tag,SurgeStorage * storage,FxStorage * fx,FxStorage * fxbuffer,int slot)395 CFxMenu::CFxMenu(const CRect &size, IControlListener *listener, long tag, SurgeStorage *storage,
396                  FxStorage *fx, FxStorage *fxbuffer, int slot)
397     : CSnapshotMenu(size, listener, tag, storage)
398 {
399     strcpy(mtype, "fx");
400     this->fx = fx;
401     this->fxbuffer = fxbuffer;
402     this->slot = slot;
403     selectedName = "";
404     populate();
405 }
406 
draw(CDrawContext * dc)407 void CFxMenu::draw(CDrawContext *dc)
408 {
409     CRect lbox = getViewSize();
410     lbox.right--;
411     lbox.bottom--;
412 
413     if (!triedToLoadBmp)
414     {
415         triedToLoadBmp = true;
416         pBackground = associatedBitmapStore->getBitmap(IDB_MENU_AS_SLIDER);
417         pBackgroundHover = skin->hoverBitmapOverlayForBackgroundBitmap(
418             skinControl, dynamic_cast<CScalableBitmap *>(pBackground), associatedBitmapStore,
419             Surge::UI::Skin::HoverType::HOVER);
420     }
421 
422     if (pBackground)
423     {
424         pBackground->draw(dc, getViewSize(), CPoint(0, 0), 0xff);
425     }
426 
427     if (isHovered && pBackgroundHover)
428     {
429         pBackgroundHover->draw(dc, getViewSize(), CPoint(0, 0), 0xff);
430     }
431 
432     // hover color and position
433     auto fgc = skin->getColor(Colors::Effect::Menu::Text);
434 
435     if (isHovered)
436         fgc = skin->getColor(Colors::Effect::Menu::TextHover);
437 
438     dc->setFontColor(fgc);
439     dc->setFont(displayFont);
440 
441     CRect txtbox(getViewSize());
442     txtbox.inset(2, 2);
443     txtbox.left += 4;
444     txtbox.right -= 12;
445     dc->drawString(fxslot_names[slot], txtbox, kLeftText, true);
446 
447     char fxname[NAMECHARS];
448     snprintf(fxname, NAMECHARS, "%s", fx_type_names[fx->type.val.i]);
449     dc->drawString(fxname, txtbox, kRightText, true);
450 
451     setDirty(false);
452 }
453 
loadSnapshot(int type,TiXmlElement * e,int idx)454 void CFxMenu::loadSnapshot(int type, TiXmlElement *e, int idx)
455 {
456     if (!type)
457         fxbuffer->type.val.i = type;
458 
459     if (e)
460     {
461         fxbuffer->type.val.i = type;
462         // storage->patch.update_controls();
463         selectedName = e->Attribute("name");
464 
465         Effect *t_fx = spawn_effect(type, storage, fxbuffer, 0);
466         if (t_fx)
467         {
468             t_fx->init_ctrltypes();
469             t_fx->init_default_values();
470             delete t_fx;
471         }
472 
473         for (int i = 0; i < n_fx_params; i++)
474         {
475             double d;
476             int j;
477             char lbl[TXT_SIZE], sublbl[TXT_SIZE];
478             snprintf(lbl, TXT_SIZE, "p%i", i);
479             if (fxbuffer->p[i].valtype == vt_float)
480             {
481                 if (e->QueryDoubleAttribute(lbl, &d) == TIXML_SUCCESS)
482                     fxbuffer->p[i].set_storage_value((float)d);
483             }
484             else
485             {
486                 if (e->QueryIntAttribute(lbl, &j) == TIXML_SUCCESS)
487                     fxbuffer->p[i].set_storage_value(j);
488             }
489 
490             snprintf(sublbl, TXT_SIZE, "p%i_temposync", i);
491             fxbuffer->p[i].temposync =
492                 ((e->QueryIntAttribute(sublbl, &j) == TIXML_SUCCESS) && (j == 1));
493             snprintf(sublbl, TXT_SIZE, "p%i_extend_range", i);
494             fxbuffer->p[i].extend_range =
495                 ((e->QueryIntAttribute(sublbl, &j) == TIXML_SUCCESS) && (j == 1));
496             snprintf(sublbl, TXT_SIZE, "p%i_deactivated", i);
497             fxbuffer->p[i].deactivated =
498                 ((e->QueryIntAttribute(sublbl, &j) == TIXML_SUCCESS) && (j == 1));
499         }
500     }
501 #if TARGET_LV2
502     auto *sge = dynamic_cast<SurgeGUIEditor *>(listenerNotForParent);
503     if (sge)
504     {
505         sge->forceautomationchangefor(&(fxbuffer->type));
506         for (int i = 0; i < n_fx_params; ++i)
507             sge->forceautomationchangefor(&(fxbuffer->p[i]));
508     }
509 #endif
510 }
saveSnapshot(TiXmlElement * e,const char * name)511 void CFxMenu::saveSnapshot(TiXmlElement *e, const char *name)
512 {
513     if (fx->type.val.i == 0)
514     {
515         return;
516     }
517 
518     TiXmlElement *t = TINYXML_SAFE_TO_ELEMENT(e->FirstChild("type"));
519 
520     while (t)
521     {
522         int ii;
523 
524         if ((t->QueryIntAttribute("i", &ii) == TIXML_SUCCESS) && (ii == fx->type.val.i))
525         {
526             // if name already exists, delete old entry
527             TiXmlElement *sn = TINYXML_SAFE_TO_ELEMENT(t->FirstChild("snapshot"));
528             while (sn)
529             {
530                 if (sn->Attribute("name") && !strcmp(sn->Attribute("name"), name))
531                 {
532                     t->RemoveChild(sn);
533                     break;
534                 }
535                 sn = TINYXML_SAFE_TO_ELEMENT(sn->NextSibling("snapshot"));
536             }
537 
538             TiXmlElement neu("snapshot");
539 
540             for (int p = 0; p < n_fx_params; p++)
541             {
542                 char lbl[TXT_SIZE], txt[TXT_SIZE], sublbl[TXT_SIZE];
543                 snprintf(lbl, TXT_SIZE, "p%i", p);
544 
545                 if (fx->p[p].ctrltype != ct_none)
546                 {
547                     neu.SetAttribute(lbl, fx->p[p].get_storage_value(txt));
548 
549                     if (fx->p[p].temposync)
550                     {
551                         snprintf(sublbl, TXT_SIZE, "p%i_temposync", p);
552                         neu.SetAttribute(sublbl, "1");
553                     }
554                     if (fx->p[p].extend_range)
555                     {
556                         snprintf(sublbl, TXT_SIZE, "p%i_extend_range", p);
557                         neu.SetAttribute(sublbl, "1");
558                     }
559                     if (fx->p[p].deactivated)
560                     {
561                         snprintf(sublbl, TXT_SIZE, "p%i_deactivated", p);
562                         neu.SetAttribute(sublbl, "1");
563                     }
564                 }
565             }
566 
567             neu.SetAttribute("name", name);
568             t->InsertEndChild(neu);
569 
570             return;
571         }
572 
573         t = TINYXML_SAFE_TO_ELEMENT(t->NextSibling("type"));
574     }
575 }
576 
577 bool CFxMenu::scanForUserPresets = true;
578 std::unordered_map<int, std::vector<CFxMenu::UserPreset>> CFxMenu::userPresets;
579 
rescanUserPresets()580 void CFxMenu::rescanUserPresets()
581 {
582     userPresets.clear();
583     scanForUserPresets = false;
584 
585     auto ud = storage->userFXPath;
586 
587     std::vector<fs::path> sfxfiles;
588 
589     std::deque<fs::path> workStack;
590     workStack.push_back(fs::path(ud));
591 
592     try
593     {
594         while (!workStack.empty())
595         {
596             auto top = workStack.front();
597             workStack.pop_front();
598             if (fs::is_directory(top))
599             {
600                 for (auto &d : fs::directory_iterator(top))
601                 {
602                     if (fs::is_directory(d))
603                     {
604                         workStack.push_back(d);
605                     }
606                     else if (path_to_string(d.path().extension()) == ".srgfx")
607                     {
608                         sfxfiles.push_back(d.path());
609                     }
610                 }
611             }
612         }
613     }
614     catch (const fs::filesystem_error &e)
615     {
616         std::ostringstream oss;
617         oss << "Experienced file system error when scanning user FX. " << e.what();
618         Surge::UserInteractions::promptError(oss.str(), "FileSystem Error");
619     }
620 
621     for (const auto &f : sfxfiles)
622     {
623         {
624             UserPreset preset;
625             preset.file = path_to_string(f);
626             TiXmlDocument d;
627             int t;
628 
629             if (!d.LoadFile(f))
630                 goto badPreset;
631 
632             auto r = TINYXML_SAFE_TO_ELEMENT(d.FirstChild("single-fx"));
633 
634             if (!r)
635                 goto badPreset;
636 
637             auto s = TINYXML_SAFE_TO_ELEMENT(r->FirstChild("snapshot"));
638 
639             if (!s)
640                 goto badPreset;
641 
642             if (!s->Attribute("name"))
643                 goto badPreset;
644 
645             preset.name = s->Attribute("name");
646 
647             if (s->QueryIntAttribute("type", &t) != TIXML_SUCCESS)
648                 goto badPreset;
649 
650             preset.type = t;
651 
652             for (int i = 0; i < n_fx_params; ++i)
653             {
654                 double fl;
655                 std::string p = "p";
656 
657                 if (s->QueryDoubleAttribute((p + std::to_string(i)).c_str(), &fl) == TIXML_SUCCESS)
658                 {
659                     preset.p[i] = fl;
660                 }
661 
662                 if (s->QueryDoubleAttribute((p + std::to_string(i) + "_temposync").c_str(), &fl) ==
663                         TIXML_SUCCESS &&
664                     fl != 0)
665                 {
666                     preset.ts[i] = true;
667                 }
668 
669                 if (s->QueryDoubleAttribute((p + std::to_string(i) + "_extend_range").c_str(),
670                                             &fl) == TIXML_SUCCESS &&
671                     fl != 0)
672                 {
673                     preset.er[i] = true;
674                 }
675 
676                 if (s->QueryDoubleAttribute((p + std::to_string(i) + "_deactivated").c_str(),
677                                             &fl) == TIXML_SUCCESS &&
678                     fl != 0)
679                 {
680                     preset.da[i] = true;
681                 }
682             }
683 
684             if (userPresets.find(preset.type) == userPresets.end())
685             {
686                 userPresets[preset.type] = std::vector<UserPreset>();
687             }
688 
689             userPresets[preset.type].push_back(preset);
690         }
691 
692     badPreset:;
693     }
694 
695     for (auto &a : userPresets)
696     {
697         std::sort(a.second.begin(), a.second.end(), [](UserPreset a, UserPreset b) {
698             if (a.type == b.type)
699             {
700                 return _stricmp(a.name.c_str(), b.name.c_str()) < 0;
701             }
702             else
703             {
704                 return a.type < b.type;
705             }
706         });
707     }
708 }
709 
populate()710 void CFxMenu::populate()
711 {
712     /*
713     ** Are there user presets
714     */
715     if (scanForUserPresets || true) // for now
716     {
717         rescanUserPresets();
718     }
719 
720     CSnapshotMenu::populate();
721 
722     /*
723     ** Add copy/paste/save
724     */
725 
726     this->addSeparator();
727 
728     auto copyItem = new CCommandMenuItem(CCommandMenuItem::Desc("Copy"));
729     auto copy = [this](CCommandMenuItem *item) { this->copyFX(); };
730     copyItem->setActions(copy, nullptr);
731     this->addEntry(copyItem);
732 
733     auto pasteItem = new CCommandMenuItem(CCommandMenuItem::Desc("Paste"));
734     auto paste = [this](CCommandMenuItem *item) { this->pasteFX(); };
735     pasteItem->setActions(paste, nullptr);
736     this->addEntry(pasteItem);
737 
738     this->addSeparator();
739 
740     if (fx->type.val.i != fxt_off)
741     {
742         auto saveItem = new CCommandMenuItem(
743             CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Save FX Preset")));
744         saveItem->setActions([this](CCommandMenuItem *item) { this->saveFX(); });
745         this->addEntry(saveItem);
746     }
747 
748     auto rescanItem = new CCommandMenuItem(
749         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Refresh FX Preset List")));
750     rescanItem->setActions([this](CCommandMenuItem *item) {
751         scanForUserPresets = true;
752         auto *sge = dynamic_cast<SurgeGUIEditor *>(listenerNotForParent);
753         if (sge)
754             sge->queueRebuildUI();
755     });
756     this->addEntry(rescanItem);
757 }
758 
copyFX()759 void CFxMenu::copyFX()
760 {
761     /*
762     ** This is a junky implementation until I figure out save and load which will require me to
763     *stream this
764     */
765     if (fxCopyPaste.size() == 0)
766     {
767         fxCopyPaste.resize(n_fx_params * 4 + 1); // type then (val; ts; extend; deact)
768     }
769 
770     fxCopyPaste[0] = fx->type.val.i;
771     for (int i = 0; i < n_fx_params; ++i)
772     {
773         int vp = i * 4 + 1;
774         int tp = i * 4 + 2;
775         int xp = i * 4 + 3;
776         int dp = i * 4 + 4;
777 
778         switch (fx->p[i].valtype)
779         {
780         case vt_float:
781             fxCopyPaste[vp] = fx->p[i].val.f;
782             break;
783         case vt_int:
784             fxCopyPaste[vp] = fx->p[i].val.i;
785             break;
786         }
787 
788         fxCopyPaste[tp] = fx->p[i].temposync;
789         fxCopyPaste[xp] = fx->p[i].extend_range;
790         fxCopyPaste[dp] = fx->p[i].deactivated;
791     }
792     memcpy((void *)fxbuffer, (void *)fx, sizeof(FxStorage));
793 }
794 
pasteFX()795 void CFxMenu::pasteFX()
796 {
797     if (fxCopyPaste.size() == 0)
798     {
799         return;
800     }
801 
802     fxbuffer->type.val.i = (int)fxCopyPaste[0];
803 
804     Effect *t_fx = spawn_effect(fxbuffer->type.val.i, storage, fxbuffer, 0);
805     if (t_fx)
806     {
807         t_fx->init_ctrltypes();
808         t_fx->init_default_values();
809         delete t_fx;
810     }
811 
812     for (int i = 0; i < n_fx_params; i++)
813     {
814         int vp = i * 4 + 1;
815         int tp = i * 4 + 2;
816         int xp = i * 4 + 3;
817         int dp = i * 4 + 4;
818 
819         switch (fxbuffer->p[i].valtype)
820         {
821         case vt_float:
822         {
823             fxbuffer->p[i].val.f = fxCopyPaste[vp];
824             if (fxbuffer->p[i].val.f < fxbuffer->p[i].val_min.f)
825             {
826                 fxbuffer->p[i].val.f = fxbuffer->p[i].val_min.f;
827             }
828             if (fxbuffer->p[i].val.f > fxbuffer->p[i].val_max.f)
829             {
830                 fxbuffer->p[i].val.f = fxbuffer->p[i].val_max.f;
831             }
832         }
833         break;
834         case vt_int:
835             fxbuffer->p[i].val.i = (int)fxCopyPaste[vp];
836             break;
837         default:
838             break;
839         }
840         fxbuffer->p[i].temposync = (int)fxCopyPaste[tp];
841         fxbuffer->p[i].extend_range = (int)fxCopyPaste[xp];
842         fxbuffer->p[i].deactivated = (int)fxCopyPaste[dp];
843     }
844 
845     selectedName = std::string("Copied ") + fx_type_names[fxbuffer->type.val.i];
846 
847     if (listenerNotForParent)
848         listenerNotForParent->valueChanged(this);
849 }
850 
saveFX()851 void CFxMenu::saveFX()
852 {
853     auto *sge = dynamic_cast<SurgeGUIEditor *>(listenerNotForParent);
854     if (sge)
855     {
856         sge->promptForMiniEdit("", "Enter a name for the FX preset:", "Save FX Preset",
857                                CPoint(-1, -1), [this](const std::string &s) { this->saveFXIn(s); });
858     }
859 }
saveFXIn(const std::string & s)860 void CFxMenu::saveFXIn(const std::string &s)
861 {
862     char fxName[TXT_SIZE];
863     fxName[0] = 0;
864     strxcpy(fxName, s.c_str(), TXT_SIZE);
865 
866     if (strlen(fxName) == 0)
867     {
868         return;
869     }
870 
871     if (!Surge::Storage::isValidName(fxName))
872     {
873         return;
874     }
875 
876     int ti = fx->type.val.i;
877 
878     std::ostringstream oss;
879     oss << storage->userFXPath << PATH_SEPARATOR << fx_type_names[ti] << PATH_SEPARATOR;
880 
881     auto pn = oss.str();
882     fs::create_directories(string_to_path(pn));
883 
884     auto fn = pn + fxName + ".srgfx";
885     std::ofstream pfile(fn, std::ios::out);
886     if (!pfile.is_open())
887     {
888         Surge::UserInteractions::promptError(
889             std::string("Unable to open FX preset file '") + fn + "' for writing!", "Error");
890         return;
891     }
892 
893     // this used to say streaming_versio before (was a typo)
894     // make sure both variants are checked when checking sv in the future on patch load
895     pfile << "<single-fx streaming_version=\"" << ff_revision << "\">\n";
896 
897     // take care of 5 special XML characters
898     std::string fxNameSub(fxName);
899     Surge::Storage::findReplaceSubstring(fxNameSub, std::string("&"), std::string("&amp;"));
900     Surge::Storage::findReplaceSubstring(fxNameSub, std::string("<"), std::string("&lt;"));
901     Surge::Storage::findReplaceSubstring(fxNameSub, std::string(">"), std::string("&gt;"));
902     Surge::Storage::findReplaceSubstring(fxNameSub, std::string("\""), std::string("&quot;"));
903     Surge::Storage::findReplaceSubstring(fxNameSub, std::string("'"), std::string("&apos;"));
904 
905     pfile << "  <snapshot name=\"" << fxNameSub.c_str() << "\" \n";
906 
907     pfile << "     type=\"" << fx->type.val.i << "\"\n";
908     for (int i = 0; i < n_fx_params; ++i)
909     {
910         if (fx->p[i].ctrltype != ct_none)
911         {
912             switch (fx->p[i].valtype)
913             {
914             case vt_float:
915                 pfile << "     p" << i << "=\"" << fx->p[i].val.f << "\"\n";
916                 break;
917             case vt_int:
918                 pfile << "     p" << i << "=\"" << fx->p[i].val.i << "\"\n";
919                 break;
920             }
921 
922             if (fx->p[i].can_temposync() && fx->p[i].temposync)
923             {
924                 pfile << "     p" << i << "_temposync=\"1\"\n";
925             }
926             if (fx->p[i].can_extend_range() && fx->p[i].extend_range)
927             {
928                 pfile << "     p" << i << "_extend_range=\"1\"\n";
929             }
930             if (fx->p[i].can_deactivate() && fx->p[i].deactivated)
931             {
932                 pfile << "     p" << i << "_deactivated=\"1\"\n";
933             }
934         }
935     }
936 
937     pfile << "  />\n";
938     pfile << "</single-fx>\n";
939     pfile.close();
940 
941     scanForUserPresets = true;
942     auto *sge = dynamic_cast<SurgeGUIEditor *>(listenerNotForParent);
943     if (sge)
944         sge->queueRebuildUI();
945 }
946 
loadUserPreset(const UserPreset & p)947 void CFxMenu::loadUserPreset(const UserPreset &p)
948 {
949     fxbuffer->type.val.i = p.type;
950 
951     Effect *t_fx = spawn_effect(fxbuffer->type.val.i, storage, fxbuffer, 0);
952 
953     if (t_fx)
954     {
955         t_fx->init_ctrltypes();
956         t_fx->init_default_values();
957         delete t_fx;
958     }
959 
960     for (int i = 0; i < n_fx_params; i++)
961     {
962         switch (fxbuffer->p[i].valtype)
963         {
964         case vt_float:
965         {
966             fxbuffer->p[i].val.f = p.p[i];
967         }
968         break;
969         case vt_int:
970             fxbuffer->p[i].val.i = (int)p.p[i];
971             break;
972         default:
973             break;
974         }
975         fxbuffer->p[i].temposync = (int)p.ts[i];
976         fxbuffer->p[i].extend_range = (int)p.er[i];
977         fxbuffer->p[i].deactivated = (int)p.da[i];
978     }
979 
980     selectedIdx = -1;
981     selectedName = p.name;
982 
983     if (listenerNotForParent)
984     {
985         listenerNotForParent->valueChanged(this);
986     }
987 }
988 
addToTopLevelTypeMenu(TiXmlElement * type,VSTGUI::COptionMenu * subMenu,int & idx)989 void CFxMenu::addToTopLevelTypeMenu(TiXmlElement *type, VSTGUI::COptionMenu *subMenu, int &idx)
990 {
991     if (!type || !subMenu)
992         return;
993 
994     int type_id = 0;
995     type->Attribute("i", &type_id);
996 
997     if (userPresets.find(type_id) == userPresets.end() || userPresets[type_id].size() == 0)
998         return;
999 
1000     auto factory_add = subMenu->addEntry("FACTORY PRESETS", 0);
1001     factory_add->setEnabled(0);
1002 
1003     auto user_add = subMenu->addEntry("USER PRESETS");
1004     user_add->setEnabled(0);
1005 
1006     for (auto &ps : userPresets[type_id])
1007     {
1008         auto fxName = ps.name;
1009 
1010 #if WINDOWS
1011         Surge::Storage::findReplaceSubstring(fxName, std::string("&"), std::string("&&"));
1012 #endif
1013 
1014         auto i = new CCommandMenuItem(CCommandMenuItem::Desc(fxName.c_str()));
1015         i->setActions([this, ps](CCommandMenuItem *item) { this->loadUserPreset(ps); });
1016         subMenu->addEntry(i);
1017     }
1018 }
1019