1 /*
2 ** Surge Synthesizer is Free and Open Source Software
3 **
4 ** Surge is made available under the Gnu General Public License, v3.0
5 ** https://www.gnu.org/licenses/gpl-3.0.en.html
6 **
7 ** Copyright 2004-2020 by various individuals as described by the Git transaction log
8 **
9 ** All source at: https://github.com/surge-synthesizer/surge.git
10 **
11 ** Surge was a commercial product from 2004-2018, with Copyright and ownership
12 ** in that period held by Claes Johanson at Vember Audio. Claes made Surge
13 ** open source in September 2018.
14 */
15 
16 #include "SurgeGUIEditor.h"
17 #include "resource.h"
18 #include "CSurgeSlider.h"
19 #include "CHSwitch2.h"
20 #include "CSwitchControl.h"
21 #include "CParameterTooltip.h"
22 #include "CPatchBrowser.h"
23 #include "COscillatorDisplay.h"
24 #include "CVerticalLabel.h"
25 #include "CModulationSourceButton.h"
26 #include "CSnapshotMenu.h"
27 #include "CLFOGui.h"
28 #include "CEffectSettings.h"
29 #include "CSurgeVuMeter.h"
30 #include "CMenuAsSlider.h"
31 #include "CEffectLabel.h"
32 #include "CTextButtonWithHover.h"
33 #include "CAboutBox.h"
34 #include "vstcontrols.h"
35 #include "SurgeBitmaps.h"
36 #include "CScalableBitmap.h"
37 #include "CNumberField.h"
38 #include "UserInteractions.h"
39 #include "DisplayInfo.h"
40 #include "UserDefaults.h"
41 #include "SkinSupport.h"
42 #include "SkinColors.h"
43 #include "UIInstrumentation.h"
44 #include "guihelpers.h"
45 #include "DebugHelpers.h"
46 #include "StringOps.h"
47 #include "ModulatorPresetManager.h"
48 
49 #include <iostream>
50 #include <iomanip>
51 #include <sstream>
52 #include <stack>
53 #include <numeric>
54 #include <unordered_map>
55 #include <codecvt>
56 #include "MSEGEditor.h"
57 #include "version.h"
58 #include "CMidiLearnOverlay.h"
59 #include "ModernOscillator.h"
60 #include "libMTSClient.h"
61 
62 #if TARGET_VST3
63 #include "pluginterfaces/vst/ivstcontextmenu.h"
64 #include "pluginterfaces/base/ustring.h"
65 
66 #include "vstgui/lib/cvstguitimer.h"
67 
68 #include "SurgeVst3Processor.h"
69 
70 template <typename T> struct RememberForgetGuard
71 {
RememberForgetGuardRememberForgetGuard72     RememberForgetGuard(T *tg)
73     {
74         t = tg;
75 
76         int rc = -1;
77         if (t)
78             rc = t->addRef();
79     }
RememberForgetGuardRememberForgetGuard80     RememberForgetGuard(const RememberForgetGuard &other)
81     {
82         int rc = -1;
83         if (t)
84         {
85             rc = t->release();
86         }
87         t = other.t;
88         if (t)
89         {
90             rc = t->addRef();
91         }
92     }
~RememberForgetGuardRememberForgetGuard93     ~RememberForgetGuard()
94     {
95         if (t)
96         {
97             t->release();
98         }
99     }
100     T *t = nullptr;
101 };
102 
103 DEF_CLASS_IID(IPlugViewContentScaleSupport);
104 
105 #endif
106 
107 #if TARGET_AUDIOUNIT
108 #include "aulayer.h"
109 #endif
110 
111 #include "filesystem/import.h"
112 
113 #if LINUX && !TARGET_JUCE_UI
114 #include "vstgui/lib/platform/platform_x11.h"
115 #include "vstgui/lib/platform/linux/x11platform.h"
116 #endif
117 
118 #if LINUX && TARGET_LV2
119 namespace SurgeLv2
120 {
121 VSTGUI::SharedPointer<VSTGUI::X11::IRunLoop> createRunLoop(void *ui);
122 }
123 #endif
124 
125 #include "RuntimeFont.h"
126 
127 const int yofs = 10;
128 
129 using namespace VSTGUI;
130 using namespace std;
131 
132 #if LINUX && TARGET_VST3
133 extern void LinuxVST3Init(Steinberg::Linux::IRunLoop *pf);
134 extern void LinuxVST3Detatch();
135 extern void LinuxVST3FrameOpen(CFrame *that, void *, const VSTGUI::PlatformType &pt);
136 extern void LinuxVST3Idle();
137 #endif
138 
139 CFontRef displayFont = NULL;
140 CFontRef patchNameFont = NULL;
141 CFontRef lfoTypeFont = NULL;
142 CFontRef aboutFont = NULL;
143 
144 enum special_tags
145 {
146     tag_scene_select = 1,
147     tag_osc_select,
148     tag_osc_menu,
149     tag_fx_select,
150     tag_fx_menu,
151     tag_patchname,
152     tag_mp_category,
153     tag_mp_patch,
154     tag_store,
155     tag_store_cancel,
156     tag_store_ok,
157     tag_store_name,
158     tag_store_category,
159     tag_store_creator,
160     tag_store_comments,
161     tag_store_tuning,
162     tag_mod_source0,
163     tag_mod_source_end = tag_mod_source0 + n_modsources,
164     tag_settingsmenu,
165     tag_mp_jogfx,
166     tag_value_typein,
167     tag_editor_overlay_close,
168     tag_miniedit_ok,
169     tag_miniedit_cancel,
170 
171     tag_status_mpe,
172     tag_status_zoom,
173     tag_status_tune,
174 
175     tag_mseg_edit,
176     tag_lfo_menu,
177     // tag_metaparam,
178     // tag_metaparam_end = tag_metaparam+n_customcontrollers,
179     start_paramtags,
180 };
181 
182 #if !TARGET_JUCE_UI
183 // TODO: CHECK REFERENCE COUNTING
184 struct SGEDropAdapter : public VSTGUI::IDropTarget, public VSTGUI::ReferenceCounted<int>
185 {
186     SurgeGUIEditor *buddy = nullptr;
SGEDropAdapterSGEDropAdapter187     SGEDropAdapter(SurgeGUIEditor *buddy)
188     {
189         this->buddy = buddy;
190         // std::cout << "Adapter created" << std::endl;
191     }
~SGEDropAdapterSGEDropAdapter192     ~SGEDropAdapter()
193     {
194         // std::cout << "Adapter Destroyed" << std::endl;
195     }
196 
singleFileFNameSGEDropAdapter197     std::string singleFileFName(VSTGUI::DragEventData data)
198     {
199         auto drag = data.drag;
200         auto where = data.pos;
201         uint32_t ct = drag->getCount();
202         if (ct == 1)
203         {
204             IDataPackage::Type t = drag->getDataType(0);
205             if (t == IDataPackage::kFilePath)
206             {
207                 const void *fn;
208                 drag->getData(0, fn, t);
209                 const char *fName = static_cast<const char *>(fn);
210                 return fName;
211             }
212         }
213         return "";
214     }
215 
onDragEnterSGEDropAdapter216     virtual VSTGUI::DragOperation onDragEnter(VSTGUI::DragEventData data) override
217     {
218         auto fn = singleFileFName(data);
219 
220         if (buddy && buddy->canDropTarget(fn))
221             return VSTGUI::DragOperation::Copy;
222         // Hand this decision off to SGE
223         return VSTGUI::DragOperation::None;
224     }
onDragMoveSGEDropAdapter225     virtual VSTGUI::DragOperation onDragMove(VSTGUI::DragEventData data) override
226     {
227         auto fn = singleFileFName(data);
228 
229         if (buddy && buddy->canDropTarget(fn))
230             return VSTGUI::DragOperation::Copy;
231 
232         return VSTGUI::DragOperation::None;
233     }
onDragLeaveSGEDropAdapter234     virtual void onDragLeave(VSTGUI::DragEventData data) override {}
onDropSGEDropAdapter235     virtual bool onDrop(VSTGUI::DragEventData data) override
236     {
237         auto fn = singleFileFName(data);
238         if (buddy)
239             return buddy->onDrop(fn);
240         return false;
241     }
242 };
243 #endif
244 
245 int SurgeGUIEditor::start_paramtag_value = start_paramtags;
246 
fromSynthGUITag(SurgeSynthesizer * synth,int tag,SurgeSynthesizer::ID & q)247 bool SurgeGUIEditor::fromSynthGUITag(SurgeSynthesizer *synth, int tag, SurgeSynthesizer::ID &q)
248 {
249     // This is wrong for macros and params but is close
250     return synth->fromSynthSideIdWithGuiOffset(tag, start_paramtags, tag_mod_source0 + ms_ctrl1, q);
251 }
252 
SurgeGUIEditor(PARENT_PLUGIN_TYPE * effect,SurgeSynthesizer * synth,void * userdata)253 SurgeGUIEditor::SurgeGUIEditor(PARENT_PLUGIN_TYPE *effect, SurgeSynthesizer *synth, void *userdata)
254     : super(effect)
255 {
256 #ifdef INSTRUMENT_UI
257     Surge::Debug::record("SurgeGUIEditor::SurgeGUIEditor");
258 #endif
259     frame = 0;
260 
261 #if TARGET_VST3
262     // setIdleRate(25);
263     // synth = ((SurgeProcessor*)effect)->getSurge();
264 #endif
265 
266     patchname = 0;
267     blinktimer = 0.f;
268     blinkstate = false;
269     aboutbox = nullptr;
270     midiLearnOverlay = nullptr;
271     patchCountdown = -1;
272 
273     mod_editor = false;
274 
275     // init the size of the plugin
276     initialZoomFactor = Surge::Storage::getUserDefaultValue(&(synth->storage), "defaultZoom", 100);
277     int instanceZoomFactor = synth->storage.getPatch().dawExtraState.editor.instanceZoomFactor;
278     if (instanceZoomFactor > 0)
279     {
280         // dawExtraState zoomFactor wins defaultZoom
281         initialZoomFactor = instanceZoomFactor;
282     }
283 
284     rect.left = 0;
285     rect.top = 0;
286 #if TARGET_VST2 || TARGET_VST3
287     rect.right = getWindowSizeX() * initialZoomFactor * 0.01;
288     rect.bottom = getWindowSizeY() * initialZoomFactor * 0.01;
289 #else
290     rect.right = getWindowSizeX();
291     rect.bottom = getWindowSizeY();
292 #endif
293     editor_open = false;
294     queue_refresh = false;
295     memset(param, 0, n_paramslots * sizeof(void *));
296     polydisp =
297         0; // FIXME - when changing skins and rebuilding we need to reset these state variables too
298     splitpointControl = 0;
299     clear_infoview_countdown = -1;
300     vu[0] = 0;
301     vu[1] = 0;
302     vu[2] = 0;
303     vu[3] = 0;
304     vu[4] = 0;
305     vu[5] = 0;
306     vu[6] = 0;
307     vu[7] = 0;
308     vu[8] = 0;
309     vu[9] = 0;
310     vu[10] = 0;
311     vu[11] = 0;
312     vu[12] = 0;
313     vu[13] = 0;
314     vu[14] = 0;
315     vu[15] = 0;
316     lfodisplay = 0;
317     fxmenu = 0;
318     for (int i = 0; i < n_fx_slots; ++i)
319     {
320         selectedFX[i] = -1;
321         fxPresetName[i] = "";
322     }
323 
324     _effect = effect;
325     _userdata = userdata;
326     this->synth = synth;
327 
328     minimumZoom = 50;
329 #if LINUX
330     minimumZoom = 100; // See github issue #628
331 #endif
332 
333     zoom_callback = [](SurgeGUIEditor *f, bool) {};
334     setZoomFactor(initialZoomFactor);
335     zoomInvalid = (initialZoomFactor != 100);
336 
337     /*
338     ** As documented in RuntimeFonts.h, the contract of this function is to side-effect
339     ** onto globals displayFont and patchNameFont with valid fonts from the runtime
340     ** distribution
341     */
342     Surge::GUI::initializeRuntimeFont();
343 
344     if (displayFont == NULL)
345     {
346         /*
347         ** OK the runtime load didn't work. Fall back to
348         ** the old defaults
349         **
350         ** FIXME: One day we will be confident enough in
351         ** our dyna loader to make this a Surge::UserInteraction::promptError
352         ** warning also.
353         **
354         ** For now, copy the defaults from above. (Don't factor this into
355         ** a function since the above defaults are initialized as dll
356         ** statics if we are not runtime).
357         */
358 #if !TARGET_JUCE_UI
359 #if MAC
360         SharedPointer<CFontDesc> minifont = new CFontDesc("Lucida Grande", 9);
361         SharedPointer<CFontDesc> patchfont = new CFontDesc("Lucida Grande", 14);
362         SharedPointer<CFontDesc> lfofont = new CFontDesc("Lucida Grande", 8);
363         SharedPointer<CFontDesc> aboutfont = new CFontDesc("Lucida Grande", 10);
364 #elif LINUX
365         SharedPointer<CFontDesc> minifont = new CFontDesc("sans-serif", 9);
366         SharedPointer<CFontDesc> patchfont = new CFontDesc("sans-serif", 14);
367         SharedPointer<CFontDesc> lfofont = new CFontDesc("sans-serif", 8);
368         SharedPointer<CFontDesc> aboutfont = new CFontDesc("sans-serif", 10);
369 #else
370         /*
371          * Choose to only warn on windows since (1) mac includes Lato in the bundle so this
372          * never happens there and (2) linux has all sorts of font noise
373          */
374         static bool warnedAboutLato =
375             Surge::Storage::getUserDefaultValue(&(synth->storage), "warnedAboutLato", 0);
376         if (!warnedAboutLato)
377         {
378             Surge::UserInteractions::promptError(
379                 std::string("Surge was unable to resolve the Lato font. ") +
380                     "Install Lato or re-run the Surge installer to resolve this. " +
381                     "Surge will run anwyay, but some of your labels may clip or be mis-rendered.",
382                 "Unable to resolve Lato Font");
383             warnedAboutLato = true;
384             Surge::Storage::updateUserDefaultValue(&(synth->storage), "warnedAboutLato", 1);
385         }
386         SharedPointer<CFontDesc> minifont = new CFontDesc("Microsoft Sans Serif", 9);
387         SharedPointer<CFontDesc> patchfont = new CFontDesc("Arial", 14);
388         SharedPointer<CFontDesc> lfofont = new CFontDesc("Microsoft Sans Serif", 8);
389         SharedPointer<CFontDesc> aboutfont = new CFontDesc("Microsoft Sans Serif", 10);
390 #endif
391 
392         displayFont = minifont;
393         patchNameFont = patchfont;
394         lfoTypeFont = lfofont;
395         aboutFont = aboutfont;
396 #endif
397     }
398 
399     /*
400     ** See the comment in SurgeParamConfig.h
401     */
402     if ((int)Surge::ParamConfig::kHorizontal != (int)VSTGUI::CSlider::kHorizontal ||
403         (int)Surge::ParamConfig::kVertical != (int)VSTGUI::CSlider::kVertical)
404     {
405         throw std::runtime_error("Software Error: Param Mismatch");
406     }
407 
408     for (int i = 0; i < n_modsources; ++i)
409         modsource_is_alternate[i] = false;
410 
411     currentSkin = Surge::UI::SkinDB::get().defaultSkin(&(this->synth->storage));
412     reloadFromSkin();
413 
414     auto des = &(synth->storage.getPatch().dawExtraState);
415     if (des->isPopulated)
416         loadFromDAWExtraState(synth);
417 }
418 
~SurgeGUIEditor()419 SurgeGUIEditor::~SurgeGUIEditor()
420 {
421     auto isPop = synth->storage.getPatch().dawExtraState.isPopulated;
422     populateDawExtraState(synth); // If I must die, leave my state for future generations
423     synth->storage.getPatch().dawExtraState.isPopulated = isPop;
424     if (frame)
425     {
426 #if !TARGET_JUCE_UI
427         getFrame()->unregisterKeyboardHook(this);
428 #endif
429         frame->close();
430     }
431 #if !TARGET_JUCE_UI
432     if (dropAdapter)
433     {
434         dropAdapter->buddy = nullptr;
435         dropAdapter->forget();
436         dropAdapter = nullptr;
437     }
438 #endif
439 
440 #if TARGET_JUCE_UI
441     frame->forget();
442 #endif
443 }
444 
idle()445 void SurgeGUIEditor::idle()
446 {
447 #if TARGET_VST2 && LINUX
448     if (!super::idle2())
449         return;
450 #endif
451 #if TARGET_VST3 && LINUX
452     LinuxVST3Idle();
453 #endif
454 #if TARGET_VST3
455     if (_effect)
456         _effect->uithreadIdleActivity();
457 #endif
458 
459     if (!synth)
460         return;
461 
462     if (pause_idle_updates)
463         return;
464 
465     if (editor_open && frame && !synth->halt_engine)
466     {
467         /*
468          * USEFUL for testing stress patch changes
469          *
470         static int runct = 0;
471         if( runct++ == 5 )
472         {
473            synth->patchid_queue = rand() % 1800;
474            runct = 0;
475         }
476           */
477         hasIdleRun = true;
478         if (firstIdleCountdown)
479         {
480             // Linux VST3 in JUCE Hosts (maybe others?) sets up the run loop out of order, it seems
481             // sometimes missing the very first invalidation. Force a redraw on the first idle
482             // isFirstIdle = false;
483             firstIdleCountdown--;
484 
485             frame->invalid();
486         }
487         if (synth->learn_param < 0 && synth->learn_custom < 0 && midiLearnOverlay != nullptr)
488         {
489             hideMidiLearnOverlay();
490         }
491         for (auto c : removeFromFrame)
492         {
493             if (frame->isChild(c))
494             {
495                 frame->removeView(c);
496             }
497         }
498         removeFromFrame.clear();
499 
500         if (clearOffscreenCachesAtZero == 0)
501         {
502             bitmapStore->clearAllBitmapOffscreenCaches();
503             frame->invalid();
504         }
505         if (clearOffscreenCachesAtZero >= 0)
506         {
507             clearOffscreenCachesAtZero--;
508         }
509 
510         {
511             bool expected = true;
512             if (synth->rawLoadNeedsUIDawExtraState.compare_exchange_weak(expected, true) &&
513                 expected)
514             {
515                 std::lock_guard<std::mutex> g(synth->rawLoadQueueMutex);
516                 synth->rawLoadNeedsUIDawExtraState = false;
517                 loadFromDAWExtraState(synth);
518             }
519         }
520 
521         if (patchCountdown >= 0 && !pause_idle_updates)
522         {
523             patchCountdown--;
524             if (patchCountdown < 0 && synth->patchid_queue >= 0)
525             {
526                 std::ostringstream oss;
527 
528                 oss << "Loading patch " << synth->patchid_queue
529                     << " has not occured after 200 idle cycles. This means"
530                     << "that the audio system is not running in your host, or that your system is "
531                        "delayed while"
532                     << " loading many patches in a row. The audio system has to be "
533                     << "running in order to load Surge patches. If the audio system is working, "
534                        "you can probably"
535                     << " ignore this message and continue once Surge catches up.";
536                 Surge::UserInteractions::promptError(oss.str(), "Patch Loading Error");
537             }
538         }
539 
540         if (zoomInvalid)
541         {
542             setZoomFactor(getZoomFactor());
543             zoomInvalid = false;
544         }
545 
546 #if TARGET_VST3
547         resizeFromIdleSentinel();
548 #endif
549 
550         if (showMSEGEditorOnNextIdleOrOpen)
551         {
552             showMSEGEditor();
553             showMSEGEditorOnNextIdleOrOpen = false;
554         }
555 
556         /*static CDrawContext drawContext
557           (frame, NULL, systemWindow);*/
558         // CDrawContext *drawContext = frame->createDrawContext();
559 
560         CView *v = frame->getFocusView();
561         if (v && dynamic_cast<CControl *>(v) != nullptr)
562         {
563             int ptag = ((CControl *)v)->getTag() - start_paramtags;
564             if (ptag >= 0)
565             {
566                 synth->storage.modRoutingMutex.lock();
567 
568                 for (int i = 1; i < n_modsources; i++)
569                 {
570                     if (gui_modsrc[i])
571                         ((CModulationSourceButton *)gui_modsrc[i])
572                             ->update_rt_vals(synth->isActiveModulation(ptag, (modsources)i), 0,
573                                              synth->isModsourceUsed((modsources)i));
574                 }
575                 synth->storage.modRoutingMutex.unlock();
576             }
577         }
578         else
579         {
580             synth->storage.modRoutingMutex.lock();
581             for (int i = 1; i < n_modsources; i++)
582             {
583                 if (gui_modsrc[i])
584                     ((CModulationSourceButton *)gui_modsrc[i])
585                         ->update_rt_vals(false, 0, synth->isModsourceUsed((modsources)i));
586             }
587             synth->storage.modRoutingMutex.unlock();
588         }
589 
590         if (synth->storage.getPatch()
591                 .scene[current_scene]
592                 .osc[current_osc[current_scene]]
593                 .wt.refresh_display)
594         {
595             synth->storage.getPatch()
596                 .scene[current_scene]
597                 .osc[current_osc[current_scene]]
598                 .wt.refresh_display = false;
599             if (oscdisplay)
600             {
601                 oscdisplay->setDirty(true);
602                 oscdisplay->invalid();
603             }
604         }
605 
606         if (typeinResetCounter > 0)
607         {
608             typeinResetCounter--;
609             if (typeinResetCounter <= 0 && typeinDialog)
610             {
611                 typeinLabel->setText(typeinResetLabel.c_str());
612                 typeinLabel->setFontColor(currentSkin->getColor(Colors::Slider::Label::Light));
613                 typeinLabel->invalid();
614             }
615         }
616 
617 #if OSC_MOD_ANIMATION
618         if (mod_editor && oscdisplay)
619         {
620             ((COscillatorDisplay *)oscdisplay)->tickModTime();
621             oscdisplay->setDirty(true);
622             oscdisplay->invalid();
623         }
624 #endif
625 
626         if (polydisp)
627         {
628             CNumberField *cnpd = static_cast<CNumberField *>(polydisp);
629             int prior = cnpd->getPoly();
630             cnpd->setPoly(synth->polydisplay);
631             if (prior != synth->polydisplay)
632                 cnpd->invalid();
633         }
634 
635         bool patchChanged = false;
636         if (patchname)
637         {
638             patchChanged = ((CPatchBrowser *)patchname)->sel_id != synth->patchid;
639         }
640 
641         if (statusMPE)
642         {
643             auto v = statusMPE->getValue();
644             if ((v < 0.5 && synth->mpeEnabled) || (v > 0.5 && !synth->mpeEnabled))
645             {
646                 statusMPE->setValue(synth->mpeEnabled ? 1 : 0);
647                 statusMPE->invalid();
648             }
649         }
650 
651         if (statusTune)
652         {
653             auto v = statusTune->getValue();
654             if ((v < 0.5 && !synth->storage.isStandardTuning) ||
655                 (v > 0.5 && synth->storage.isStandardTuning))
656             {
657                 bool hasmts =
658                     synth->storage.oddsound_mts_client && synth->storage.oddsound_mts_active;
659                 statusTune->setValue(!synth->storage.isStandardTuning || hasmts);
660                 statusTune->invalid();
661             }
662         }
663 
664         if (patchChanged)
665         {
666 #if TARGET_AUDIOUNIT
667             synth->getParent()->setPresetByID(synth->patchid);
668 #endif
669             for (int i = 0; i < n_fx_slots; ++i)
670             {
671                 fxPresetName[i] = "";
672             }
673         }
674 
675         if (queue_refresh || synth->refresh_editor || patchChanged)
676         {
677             queue_refresh = false;
678             synth->refresh_editor = false;
679 
680             if (frame)
681             {
682                 if (synth->patch_loaded)
683                     mod_editor = false;
684                 synth->patch_loaded = false;
685 
686                 openOrRecreateEditor();
687             }
688             if (patchname)
689             {
690                 ((CPatchBrowser *)patchname)->sel_id = synth->patchid;
691                 ((CPatchBrowser *)patchname)->setLabel(synth->storage.getPatch().name);
692                 ((CPatchBrowser *)patchname)->setCategory(synth->storage.getPatch().category);
693                 ((CPatchBrowser *)patchname)->setAuthor(synth->storage.getPatch().author);
694                 patchname->invalid();
695             }
696         }
697 
698         bool vuInvalid = false;
699         if (synth->vu_peak[0] != vu[0]->getValue())
700         {
701             vuInvalid = true;
702             vu[0]->setValue(synth->vu_peak[0]);
703         }
704         if (synth->vu_peak[1] != ((CSurgeVuMeter *)vu[0])->getValueR())
705         {
706             ((CSurgeVuMeter *)vu[0])->setValueR(synth->vu_peak[1]);
707             vuInvalid = true;
708         }
709         if (vuInvalid)
710             vu[0]->invalid();
711 
712         for (int i = 0; i < n_fx_slots; i++)
713         {
714             assert(i + 1 < Effect::KNumVuSlots);
715             if (vu[i + 1] && synth->fx[current_fx])
716             {
717                 // there's seems to be a bug here that overwrites either this or the vu-pointer
718                 // try to catch it earlier to retrieve more info
719 
720                 // assert(!((int)vu[i+1] & 0xffff0000));
721 
722                 // check so it doesn't overlap with the infowindow
723                 CRect iw = ((CParameterTooltip *)infowindow)->getViewSize();
724                 CRect vur = vu[i + 1]->getViewSize();
725 
726                 if (!((CParameterTooltip *)infowindow)->isVisible() || !vur.rectOverlap(iw))
727                 {
728                     vu[i + 1]->setValue(synth->fx[current_fx]->vu[(i << 1)]);
729                     ((CSurgeVuMeter *)vu[i + 1])
730                         ->setValueR(synth->fx[current_fx]->vu[(i << 1) + 1]);
731                     vu[i + 1]->invalid();
732                 }
733             }
734         }
735 
736         for (int i = 0; i < 8; i++)
737         {
738             if (synth->refresh_ctrl_queue[i] >= 0)
739             {
740                 int j = synth->refresh_ctrl_queue[i];
741                 synth->refresh_ctrl_queue[i] = -1;
742 
743                 if (param[j])
744                 {
745                     char pname[256], pdisp[256];
746                     SurgeSynthesizer::ID jid;
747                     if (synth->fromSynthSideId(j, jid))
748                     {
749                         synth->getParameterName(jid, pname);
750                         synth->getParameterDisplay(jid, pdisp);
751                     }
752 
753                     /*if(i == 0)
754                       {
755                       ((gui_pdisplay*)infowindow)->setLabel(pname,pdisp);
756                       draw_infowindow(j, param[j], false, true);
757                       clear_infoview_countdown = 40;
758                       }*/
759 
760                     param[j]->setValue(synth->refresh_ctrl_queue_value[i]);
761                     frame->invalidRect(param[j]->getViewSize());
762                     // oscdisplay->invalid();
763 
764                     if (oscdisplay)
765                     {
766                         ((COscillatorDisplay *)oscdisplay)->invalidateIfIdIsInRange(j);
767                     }
768 
769                     if (lfodisplay)
770                     {
771                         ((CLFOGui *)lfodisplay)->invalidateIfIdIsInRange(j);
772                     }
773                 }
774             }
775         }
776 
777         if (lastTempo != synth->time_data.tempo || lastTSNum != synth->time_data.timeSigNumerator ||
778             lastTSDen != synth->time_data.timeSigDenominator)
779         {
780             lastTempo = synth->time_data.tempo;
781             lastTSNum = synth->time_data.timeSigNumerator;
782             lastTSDen = synth->time_data.timeSigDenominator;
783             if (lfodisplay)
784             {
785                 ((CLFOGui *)lfodisplay)
786                     ->setTimeSignature(synth->time_data.timeSigNumerator,
787                                        synth->time_data.timeSigDenominator);
788                 ((CLFOGui *)lfodisplay)->invalidateIfAnythingIsTemposynced();
789             }
790         }
791 
792         std::vector<int> refreshIndices;
793         if (synth->refresh_overflow)
794         {
795             refreshIndices.resize(n_total_params);
796             std::iota(std::begin(refreshIndices), std::end(refreshIndices), 0);
797             frame->invalid();
798         }
799         else
800         {
801             for (int i = 0; i < 8; ++i)
802                 if (synth->refresh_parameter_queue[i] >= 0)
803                     refreshIndices.push_back(synth->refresh_parameter_queue[i]);
804         }
805 
806         synth->refresh_overflow = false;
807         for (int i = 0; i < 8; ++i)
808             synth->refresh_parameter_queue[i] = -1;
809 
810         for (auto j : refreshIndices)
811         {
812             if ((j < n_total_params) && param[j])
813             {
814                 SurgeSynthesizer::ID jid;
815                 if (synth->fromSynthSideId(j, jid))
816                     param[j]->setValue(synth->getParameter01(jid));
817                 frame->invalidRect(param[j]->getViewSize());
818 
819                 if (oscdisplay)
820                 {
821                     ((COscillatorDisplay *)oscdisplay)->invalidateIfIdIsInRange(j);
822                 }
823 
824                 if (lfodisplay)
825                 {
826                     ((CLFOGui *)lfodisplay)->invalidateIfIdIsInRange(j);
827                 }
828             }
829             else if ((j >= metaparam_offset) && (j < (metaparam_offset + n_customcontrollers)))
830             {
831                 int cc = j - metaparam_offset;
832                 gui_modsrc[ms_ctrl1 + cc]->setValue(
833                     ((ControllerModulationSource *)synth->storage.getPatch()
834                          .scene[current_scene]
835                          .modsources[ms_ctrl1 + cc])
836                         ->get_target01());
837             }
838             else if ((j >= 0) && (j < n_total_params) && nonmod_param[j])
839             {
840                 /*
841                 ** What the heck is this NONMOD_PARAM thing?
842                 **
843                 ** There are a set of params - like discrete things like
844                 ** octave and filter type - which are not LFO modulatable
845                 ** and aren't in the params[] array. But they are exposed
846                 ** properties, so you can control them from a DAW. The
847                 ** DAW control works - everything up to this path (as described
848                 ** in #160) works fine and sets the value but since there's
849                 ** no CControl in param the above bails out. But ading
850                 ** all these controls to param[] would have the unintended
851                 ** side effect of giving them all the other param[] behaviours.
852                 ** So have a second array and drop select items in here so we
853                 ** can actually get them redrawing when an external param set occurs.
854                 */
855                 CControl *cc = nonmod_param[j];
856 
857                 /*
858                 ** Some state changes enable and disable sliders. If this is one of those state
859                 *changes and a value has changed, then
860                 ** we need to invalidate them. See #2056.
861                 */
862                 auto tag = cc->getTag();
863                 SurgeSynthesizer::ID jid;
864 
865                 auto sv = 0.f;
866                 if (synth->fromSynthSideId(j, jid))
867                     sv = synth->getParameter01(jid);
868                 auto cv = cc->getValue();
869 
870                 if ((sv != cv) && ((tag == fmconfig_tag || tag == filterblock_tag)))
871                 {
872                     std::unordered_map<int, bool> resetMap;
873 
874                     if (tag == fmconfig_tag)
875                     {
876                         auto targetTag =
877                             synth->storage.getPatch().scene[current_scene].fm_depth.id +
878                             start_paramtags;
879                         auto targetState =
880                             (Parameter::intUnscaledFromFloat(sv, n_fm_routings - 1) == fm_off);
881                         resetMap[targetTag] = targetState;
882                     }
883 
884                     if (tag == filterblock_tag)
885                     {
886                         auto pval = Parameter::intUnscaledFromFloat(sv, n_filter_configs - 1);
887 
888                         auto targetTag =
889                             synth->storage.getPatch().scene[current_scene].feedback.id +
890                             start_paramtags;
891                         auto targetState = (pval == fc_serial1);
892                         resetMap[targetTag] = targetState;
893 
894                         targetTag = synth->storage.getPatch().scene[current_scene].width.id +
895                                     start_paramtags;
896                         targetState = (pval != fc_stereo && pval != fc_wide);
897                         resetMap[targetTag] = targetState;
898                     }
899 
900                     for (int i = 0; i < n_paramslots; i++)
901                     {
902                         if (param[i] && (resetMap.find(param[i]->getTag()) != resetMap.end()))
903                         {
904                             auto css = dynamic_cast<CSurgeSlider *>(param[i]);
905 
906                             if (css)
907                             {
908                                 css->disabled = resetMap[param[i]->getTag()];
909                                 css->invalid();
910                             }
911                         }
912                     }
913                 }
914 
915                 if (synth->storage.getPatch().param_ptr[j]->ctrltype == ct_scenemode)
916                 {
917                     /*
918                      * This is gross hack for our reordering of scenemode. Basically take the
919                      * automation value and turn it into the UI value
920                      */
921                     auto pval = Parameter::intUnscaledFromFloat(sv, n_scene_modes - 1);
922                     if (pval == sm_dual)
923                         pval = sm_chsplit;
924                     else if (pval == sm_chsplit)
925                         pval = sm_dual;
926                     sv = Parameter::intScaledToFloat(pval, n_scene_modes - 1);
927                 }
928 
929                 if (sv != cv)
930                 {
931                     cc->setValue(sv);
932                 }
933 
934                 // Integer switches also work differently
935                 auto assw = dynamic_cast<CSwitchControl *>(cc);
936                 if (assw)
937                 {
938                     if (assw->is_itype)
939                     {
940                         assw->ivalue = synth->storage.getPatch().param_ptr[j]->val.i + 1;
941                     }
942                 }
943 
944                 cc->setDirty();
945                 cc->invalid();
946             }
947 #if 0
948          /*
949          ** A set of special things which invalidate the entire UI.
950          ** See #2226 for why this is currently off.
951          */
952          else if( j == synth->storage.getPatch().scene_active.id )
953          {
954             if( current_scene != synth->storage.getPatch().scene_active.val.i )
955             {
956                synth->refresh_editor = true;
957                current_scene = synth->storage.getPatch().scene_active.val.i;
958             }
959 
960          }
961 #endif
962             else
963             {
964                 // printf( "Bailing out of all possible refreshes on %d\n", j );
965                 // This is not really a problem
966             }
967         }
968         for (int i = 0; i < n_customcontrollers; i++)
969         {
970             if (((ControllerModulationSource *)synth->storage.getPatch()
971                      .scene[current_scene]
972                      .modsources[ms_ctrl1 + i])
973                     ->has_changed(true))
974             {
975                 gui_modsrc[ms_ctrl1 + i]->setValue(
976                     ((ControllerModulationSource *)synth->storage.getPatch()
977                          .scene[current_scene]
978                          .modsources[ms_ctrl1 + i])
979                         ->get_target01());
980             }
981         }
982         clear_infoview_countdown += clear_infoview_peridle;
983         if (clear_infoview_countdown == 0)
984         {
985             ((CParameterTooltip *)infowindow)->Hide();
986             // infowindow->getViewSize();
987             // ctnvg       frame->redrawRect(drawContext,r);
988         }
989 
990         // frame->update(&drawContext);
991         // frame->idle();
992     }
993 
994 #if BUILD_IS_DEBUG
995     if (debugLabel)
996     {
997         /*
998          * We can do debuggy stuff here to get an indea about internal state on the screen
999          */
1000 
1001         /*
1002           auto t = std::to_string( synth->storage.songpos );
1003           debugLabel->setText(t.c_str());
1004           debugLabel->invalid();
1005           */
1006     }
1007 #endif
1008 }
1009 
toggle_mod_editing()1010 void SurgeGUIEditor::toggle_mod_editing()
1011 {
1012     bool doModEditing = true;
1013 
1014     if (currentSkin->getVersion() >= 2)
1015     {
1016         auto skinCtrl = currentSkin->controlForUIID("controls.modulation.panel");
1017 
1018         if (!skinCtrl)
1019         {
1020             skinCtrl = currentSkin->getOrCreateControlForConnector(
1021                 Surge::Skin::Connector::connectorByID("controls.modulation.panel"));
1022         }
1023 
1024         if (skinCtrl->classname == Surge::UI::NoneClassName)
1025         {
1026             doModEditing = false;
1027         }
1028     }
1029 
1030     if (doModEditing)
1031     {
1032         mod_editor = !mod_editor;
1033         refresh_mod();
1034 
1035         auto iw = dynamic_cast<CParameterTooltip *>(infowindow);
1036 
1037         if (iw && iw->isVisible())
1038         {
1039             iw->Hide();
1040         }
1041     }
1042 }
1043 
refresh_mod()1044 void SurgeGUIEditor::refresh_mod()
1045 {
1046     CModulationSourceButton *cms = gui_modsrc[modsource];
1047 
1048     modsources thisms = modsource;
1049     if (cms->hasAlternate && cms->useAlternate)
1050         thisms = (modsources)cms->alternateId;
1051 
1052     synth->storage.modRoutingMutex.lock();
1053     for (int i = 0; i < n_paramslots; i++)
1054     {
1055         if (param[i])
1056         {
1057             auto *s = dynamic_cast<CSurgeSlider *>(param[i]);
1058             if (s)
1059             {
1060                 if (s->is_mod)
1061                 {
1062                     s->setModMode(mod_editor ? 1 : 0);
1063                     s->setModPresent(synth->isModDestUsed(i));
1064                     s->setModCurrent(synth->isActiveModulation(i, thisms),
1065                                      synth->isBipolarModulation(thisms));
1066                 }
1067                 // s->setDirty();
1068                 s->setModValue(synth->getModulation(i, thisms));
1069                 s->invalid();
1070             }
1071         }
1072     }
1073 #if OSC_MOD_ANIMATION
1074     if (oscdisplay)
1075     {
1076         ((COscillatorDisplay *)oscdisplay)->setIsMod(mod_editor);
1077         ((COscillatorDisplay *)oscdisplay)->setModSource(thisms);
1078         oscdisplay->invalid();
1079         oscdisplay->setDirty(true);
1080     }
1081 #endif
1082 
1083     synth->storage.modRoutingMutex.unlock();
1084 
1085     for (int i = 1; i < n_modsources; i++)
1086     {
1087         int state = 0;
1088 
1089         if (i == modsource)
1090             state = mod_editor ? 2 : 1;
1091 
1092         if (i == modsource_editor[current_scene] && lfoNameLabel)
1093         {
1094             state |= 4;
1095 
1096             // update the LFO title label
1097             std::string modname = modulatorName(modsource_editor[current_scene], true);
1098 
1099             lfoNameLabel->setText(modname.c_str());
1100         }
1101 
1102         if (gui_modsrc[i])
1103         {
1104             // this could change if I cleared the last one
1105             gui_modsrc[i]->used = synth->isModsourceUsed((modsources)i);
1106             gui_modsrc[i]->state = state;
1107 
1108             if (i < ms_ctrl1 || i > ms_ctrl8)
1109             {
1110                 auto mn = modulatorName(i, true);
1111                 gui_modsrc[i]->setlabel(mn.c_str());
1112             }
1113             gui_modsrc[i]->invalid();
1114         }
1115     }
1116 // ctnvg frame->redraw();
1117 // frame->setDirty();
1118 #if LINUX
1119     frame->invalid();
1120     // Turns out linux is very bad with lots of little invalid rects in vstgui
1121     // see github issue 1103
1122 #endif
1123 }
1124 
onKeyDown(const VstKeyCode & code,CFrame * frame)1125 int32_t SurgeGUIEditor::onKeyDown(const VstKeyCode &code, CFrame *frame)
1126 {
1127 #if !TARGET_JUCE_UI
1128     if (code.virt != 0)
1129     {
1130         switch (code.virt)
1131         {
1132         case VKEY_F5:
1133             if (Surge::Storage::getUserDefaultValue(&(this->synth->storage), "skinReloadViaF5", 0))
1134             {
1135                 bitmapStore.reset(new SurgeBitmaps());
1136                 bitmapStore->setupBitmapsForFrame(frame);
1137 
1138                 if (!currentSkin->reloadSkin(bitmapStore))
1139                 {
1140                     auto &db = Surge::UI::SkinDB::get();
1141                     auto msg = std::string("Unable to load skin! Reverting the skin to Surge "
1142                                            "Classic.\n\nSkin error:\n") +
1143                                db.getAndResetErrorString();
1144                     currentSkin = db.defaultSkin(&(synth->storage));
1145                     currentSkin->reloadSkin(bitmapStore);
1146                     Surge::UserInteractions::promptError(msg, "Skin Loading Error");
1147                 }
1148 
1149                 reloadFromSkin();
1150                 synth->refresh_editor = true;
1151             }
1152 
1153             return 1;
1154             break;
1155         case VKEY_TAB:
1156             if ((topmostEditorTag() == STORE_PATCH) || (typeinDialog && typeinDialog->isVisible()))
1157             {
1158                 /*
1159                 ** SaveDialog gets access to the tab key to switch between fields if it is open
1160                 */
1161                 return -1;
1162             }
1163             toggle_mod_editing();
1164             return 1;
1165             break;
1166         case VKEY_ESCAPE:
1167             if (typeinDialog && typeinDialog->isVisible())
1168             {
1169                 // important to do this first since it basically stops us listening to changes
1170                 typeinEditTarget = nullptr;
1171 
1172                 typeinDialog->setVisible(false);
1173                 removeFromFrame.push_back(typeinDialog);
1174                 typeinDialog = nullptr;
1175                 typeinMode = Inactive;
1176                 typeinResetCounter = -1;
1177 
1178                 return 1;
1179             }
1180             break;
1181         case VKEY_RETURN:
1182             if (typeinDialog && typeinDialog->isVisible())
1183             {
1184                 typeinDialog->setVisible(false);
1185             }
1186             break;
1187         }
1188     }
1189 #endif
1190     return -1;
1191 }
1192 
onKeyUp(const VstKeyCode & keyCode,CFrame * frame)1193 int32_t SurgeGUIEditor::onKeyUp(const VstKeyCode &keyCode, CFrame *frame) { return -1; }
1194 
isControlVisible(ControlGroup controlGroup,int controlGroupEntry)1195 bool SurgeGUIEditor::isControlVisible(ControlGroup controlGroup, int controlGroupEntry)
1196 {
1197     switch (controlGroup)
1198     {
1199     case cg_OSC:
1200         return (controlGroupEntry == current_osc[current_scene]);
1201     case cg_LFO:
1202         return (controlGroupEntry == modsource_editor[current_scene]);
1203     case cg_FX:
1204         return (controlGroupEntry == current_fx);
1205     default:
1206         return true;
1207     }
1208 }
1209 
positionForModulationGrid(modsources entry)1210 CRect SurgeGUIEditor::positionForModulationGrid(modsources entry)
1211 {
1212     bool isMacro = isCustomController(entry);
1213     int gridX = modsource_grid_xy[entry][0];
1214     int gridY = modsource_grid_xy[entry][1];
1215     int width = isMacro ? 93 : 74;
1216 
1217     // to ensure the same gap between the modbuttons,
1218     // make the first and last non-macro button wider 2px
1219     if ((!isMacro) && ((gridX == 0) || (gridX == 9)))
1220         width += 2;
1221 
1222     auto skinCtrl = currentSkin->controlForUIID("controls.modulation.panel");
1223 
1224     if (!skinCtrl)
1225     {
1226         skinCtrl = currentSkin->getOrCreateControlForConnector(
1227             Surge::Skin::Connector::connectorByID("controls.modulation.panel"));
1228     }
1229 
1230     if (skinCtrl->classname == Surge::UI::NoneClassName && currentSkin->getVersion() >= 2)
1231     {
1232         return CRect();
1233     }
1234 
1235     CRect r(CPoint(skinCtrl->x, skinCtrl->y), CPoint(width - 1, 14));
1236 
1237     if (isMacro)
1238         r.bottom += 8;
1239 
1240     int offsetX = 1;
1241 
1242     for (int i = 0; i < gridX; i++)
1243     {
1244         if ((!isMacro) && (i == 0))
1245             offsetX += 2;
1246 
1247         // gross hack for accumulated 2 px horizontal offsets from the previous if clause
1248         // needed to align the last column nicely
1249         if ((!isMacro) && (i == 8))
1250             offsetX -= 18;
1251 
1252         offsetX += width;
1253     }
1254 
1255     r.offset(offsetX, (8 * gridY));
1256 
1257     return r;
1258 }
1259 
setDisabledForParameter(Parameter * p,CSurgeSlider * s)1260 void SurgeGUIEditor::setDisabledForParameter(Parameter *p, CSurgeSlider *s)
1261 {
1262     if (p->id == synth->storage.getPatch().scene[current_scene].fm_depth.id)
1263     {
1264         s->disabled = !(synth->storage.getPatch().scene[current_scene].fm_switch.val.i);
1265     }
1266 }
1267 
1268 class LastChanceEventCapture : public CControl
1269 {
1270   public:
LastChanceEventCapture(const CPoint & sz,SurgeGUIEditor * ed)1271     LastChanceEventCapture(const CPoint &sz, SurgeGUIEditor *ed)
1272         : CControl(CRect(CPoint(0, 0), sz)), editor(ed)
1273     {
1274     }
draw(CDrawContext * pContext)1275     void draw(CDrawContext *pContext) override { return; }
1276 
onMouseDown(CPoint & where,const CButtonState & buttons)1277     CMouseEventResult onMouseDown(CPoint &where, const CButtonState &buttons) override
1278     {
1279         if (buttons & (kMButton | kButton4 | kButton5))
1280         {
1281             if (editor)
1282             {
1283                 editor->toggle_mod_editing();
1284             }
1285 
1286             return kMouseEventHandled;
1287         }
1288 
1289         if (buttons & kRButton)
1290         {
1291             if (editor)
1292             {
1293                 CRect menuRect;
1294                 menuRect.offset(where.x, where.y);
1295 
1296                 editor->useDevMenu = false;
1297                 editor->showSettingsMenu(menuRect);
1298             }
1299 
1300             return kMouseEventHandled;
1301         }
1302 
1303         return kMouseEventNotHandled;
1304     }
1305 
1306   private:
1307     SurgeGUIEditor *editor = nullptr;
1308     CLASS_METHODS(LastChanceEventCapture, CControl);
1309 };
1310 
openOrRecreateEditor()1311 void SurgeGUIEditor::openOrRecreateEditor()
1312 {
1313 #ifdef INSTRUMENT_UI
1314     Surge::Debug::record("SurgeGUIEditor::openOrRecreateEditor");
1315 #endif
1316     if (!synth)
1317         return;
1318     assert(frame);
1319 
1320     removeFromFrame.clear();
1321 
1322     editorOverlayTagAtClose = "";
1323     if (editor_open)
1324     {
1325         editorOverlayTagAtClose = topmostEditorTag();
1326         for (auto el : editorOverlay)
1327         {
1328             el.second->remember();
1329             frame->removeView(el.second);
1330         }
1331 
1332         close_editor();
1333     }
1334     CPoint nopoint(0, 0);
1335     CPoint sz(getWindowSizeX(), getWindowSizeY());
1336     auto lcb = new LastChanceEventCapture(sz, this);
1337     frame->addView(lcb);
1338 
1339     clear_infoview_peridle = -1;
1340 
1341     std::unordered_map<std::string, std::string> uiidToSliderLabel;
1342 
1343     /*
1344     ** There are a collection of member states we need to reset
1345     */
1346     polydisp = nullptr;
1347     lfodisplay = nullptr;
1348     fxmenu = nullptr;
1349     typeinDialog = nullptr;
1350     minieditOverlay = nullptr;
1351     msegEditSwitch = nullptr;
1352     lfoNameLabel = nullptr;
1353     midiLearnOverlay = nullptr;
1354 
1355     for (int i = 0; i < 16; ++i)
1356         vu[i] = nullptr;
1357 
1358     current_scene = synth->storage.getPatch().scene_active.val.i;
1359 
1360     /*
1361      * In Surge 1.8, the skin engine can change the position of this panel as a whole
1362      * but not anything else about it. The skin query happens inside positionForModulationGrid
1363      */
1364     for (int k = 1; k < n_modsources; k++)
1365     {
1366         if ((k != ms_random_unipolar) && (k != ms_alternate_unipolar))
1367         {
1368             modsources ms = (modsources)k;
1369 
1370             CRect r = positionForModulationGrid(ms);
1371 
1372             int state = 0;
1373 
1374             if (ms == modsource)
1375                 state = mod_editor ? 2 : 1;
1376             if (ms == modsource_editor[current_scene])
1377                 state |= 4;
1378 
1379             gui_modsrc[ms] = new CModulationSourceButton(r, this, tag_mod_source0 + ms, state, ms,
1380                                                          bitmapStore, &(synth->storage));
1381 
1382             gui_modsrc[ms]->setSkin(currentSkin);
1383             gui_modsrc[ms]->update_rt_vals(false, 0, synth->isModsourceUsed(ms));
1384 
1385             if ((ms >= ms_ctrl1) && (ms <= ms_ctrl8))
1386             {
1387                 // std::cout << synth->learn_custom << std::endl;
1388                 gui_modsrc[ms]->setlabel(
1389                     synth->storage.getPatch().CustomControllerLabel[ms - ms_ctrl1]);
1390                 gui_modsrc[ms]->set_ismeta(true);
1391                 gui_modsrc[ms]->setBipolar(
1392                     synth->storage.getPatch().scene[current_scene].modsources[ms]->is_bipolar());
1393                 gui_modsrc[ms]->setValue(((ControllerModulationSource *)synth->storage.getPatch()
1394                                               .scene[current_scene]
1395                                               .modsources[ms])
1396                                              ->get_target01());
1397             }
1398             else
1399             {
1400                 gui_modsrc[ms]->setlabel(modulatorName(ms, true).c_str());
1401 
1402                 if (ms == ms_random_bipolar)
1403                 {
1404                     gui_modsrc[ms]->setAlternate(ms_random_unipolar,
1405                                                  modsource_names_button[ms_random_unipolar]);
1406                     gui_modsrc[ms]->setUseAlternate(modsource_is_alternate[ms]);
1407                 }
1408 
1409                 if (ms == ms_alternate_bipolar)
1410                 {
1411                     gui_modsrc[ms]->setAlternate(ms_alternate_unipolar,
1412                                                  modsource_names_button[ms_alternate_unipolar]);
1413                     gui_modsrc[ms]->setUseAlternate(modsource_is_alternate[ms]);
1414                 }
1415             }
1416 
1417             frame->addView(gui_modsrc[ms]);
1418             if (ms >= ms_ctrl1 && ms <= ms_ctrl8 && synth->learn_custom == ms - ms_ctrl1)
1419             {
1420                 showMidiLearnOverlay(r);
1421             }
1422         }
1423     }
1424 
1425     /*// Comments
1426       {
1427       CRect CommentsRect(6 + 150*4,528, getWindowSizeX(), getWindowSizeY());
1428       CTextLabel *Comments = new
1429       CTextLabel(CommentsRect,synth->storage.getPatch().comment.c_str());
1430       Comments->setTransparency(true);
1431       Comments->setFont(displayFont);
1432       Comments->setHoriAlign(kMultiLineCenterText);
1433       frame->addView(Comments);
1434       }*/
1435 
1436     // fx vu-meters & labels. This is all a bit hacky still
1437     {
1438         std::lock_guard<std::mutex> g(synth->fxSpawnMutex);
1439 
1440         if (synth->fx[current_fx])
1441         {
1442             auto fxpp = currentSkin->getOrCreateControlForConnector("fx.param.panel");
1443             CRect fxRect = CRect(CPoint(fxpp->x, fxpp->y), CPoint(123, 13));
1444             for (int i = 0; i < 15; i++)
1445             {
1446                 int t = synth->fx[current_fx]->vu_type(i);
1447                 if (t)
1448                 {
1449                     CRect vr(fxRect); // FIXME (vurect);
1450                     vr.offset(6, yofs * synth->fx[current_fx]->vu_ypos(i));
1451                     vr.offset(0, -14);
1452                     vu[i + 1] = new CSurgeVuMeter(vr, this);
1453                     ((CSurgeVuMeter *)vu[i + 1])->setSkin(currentSkin, bitmapStore);
1454                     ((CSurgeVuMeter *)vu[i + 1])->setType(t);
1455                     frame->addView(vu[i + 1]);
1456                 }
1457                 else
1458                 {
1459                     vu[i + 1] = 0;
1460                 }
1461 
1462                 const char *label = synth->fx[current_fx]->group_label(i);
1463 
1464                 if (label)
1465                 {
1466                     CRect vr(fxRect); // (vurect);
1467                     vr.top += 1;
1468                     vr.right += 5;
1469                     vr.offset(5, -12);
1470                     vr.offset(0, yofs * synth->fx[current_fx]->group_label_ypos(i));
1471                     CEffectLabel *lb = new CEffectLabel(vr);
1472                     lb->setLabel(label);
1473                     lb->setSkin(currentSkin, bitmapStore);
1474                     frame->addView(lb);
1475                 }
1476             }
1477         }
1478     }
1479 
1480     /*
1481      * Loop over the non-associated controls
1482      */
1483     for (auto i = Surge::Skin::Connector::NonParameterConnection::PARAMETER_CONNECTED + 1;
1484          i < Surge::Skin::Connector::NonParameterConnection::N_NONCONNECTED; ++i)
1485     {
1486         Surge::Skin::Connector::NonParameterConnection npc =
1487             (Surge::Skin::Connector::NonParameterConnection)i;
1488         auto conn = Surge::Skin::Connector::connectorByNonParameterConnection(npc);
1489         auto skinCtrl = currentSkin->getOrCreateControlForConnector(conn);
1490         currentSkin->resolveBaseParentOffsets(skinCtrl);
1491 
1492         if (!skinCtrl)
1493         {
1494             std::cout << "Unable to find SkinCtrl" << std::endl;
1495             continue;
1496         }
1497         if (skinCtrl->classname == Surge::UI::NoneClassName)
1498             continue;
1499 
1500         /*
1501          * Many of the controls are special and so require non-generalizable constructors
1502          * handled here. Some are standard and so once we know the tag we can use
1503          * layoutComponentForSkin but it's not worth generalizing the OSCILLATOR_DISPLAY beyond
1504          * this, say.
1505          */
1506         switch (npc)
1507         {
1508         case Surge::Skin::Connector::NonParameterConnection::OSCILLATOR_DISPLAY:
1509         {
1510             auto od = new COscillatorDisplay(
1511                 CRect(CPoint(skinCtrl->x, skinCtrl->y), CPoint(skinCtrl->w, skinCtrl->h)), this,
1512                 &synth->storage.getPatch()
1513                      .scene[synth->storage.getPatch().scene_active.val.i]
1514                      .osc[current_osc[current_scene]],
1515                 &synth->storage);
1516             od->setSkin(currentSkin, bitmapStore);
1517             frame->addView(od);
1518             oscdisplay = od;
1519             break;
1520         }
1521         case Surge::Skin::Connector::NonParameterConnection::SURGE_MENU:
1522         {
1523             auto q = layoutComponentForSkin(skinCtrl, tag_settingsmenu);
1524             break;
1525         }
1526         case Surge::Skin::Connector::NonParameterConnection::OSCILLATOR_SELECT:
1527         {
1528             auto oscswitch = layoutComponentForSkin(skinCtrl, tag_osc_select);
1529             oscswitch->setValue((float)current_osc[current_scene] / 2.0f);
1530             break;
1531         }
1532         case Surge::Skin::Connector::NonParameterConnection::JOG_PATCHCATEGORY:
1533         {
1534             layoutComponentForSkin(skinCtrl, tag_mp_category);
1535             break;
1536         }
1537         case Surge::Skin::Connector::NonParameterConnection::JOG_PATCH:
1538         {
1539             layoutComponentForSkin(skinCtrl, tag_mp_patch);
1540             break;
1541         }
1542         case Surge::Skin::Connector::NonParameterConnection::JOG_FX:
1543         {
1544             layoutComponentForSkin(skinCtrl, tag_mp_jogfx);
1545             break;
1546         }
1547         case Surge::Skin::Connector::NonParameterConnection::STATUS_MPE:
1548         {
1549             statusMPE = layoutComponentForSkin(skinCtrl, tag_status_mpe);
1550             statusMPE->setValue(synth->mpeEnabled ? 1 : 0);
1551             break;
1552         }
1553         case Surge::Skin::Connector::NonParameterConnection::STATUS_TUNE:
1554         {
1555             // FIXME - drag and drop onto this?
1556             statusTune = layoutComponentForSkin(skinCtrl, tag_status_tune);
1557             auto hasmts = synth->storage.oddsound_mts_client && synth->storage.oddsound_mts_active;
1558             statusTune->setValue(synth->storage.isStandardTuning ? hasmts : 1);
1559 
1560             auto csc = dynamic_cast<CSwitchControl *>(statusTune);
1561             if (csc && synth->storage.isStandardTuning)
1562             {
1563                 csc->unValueClickable = !synth->storage.isToggledToCache;
1564             }
1565             break;
1566         }
1567         case Surge::Skin::Connector::NonParameterConnection::STATUS_ZOOM:
1568         {
1569             statusZoom = layoutComponentForSkin(skinCtrl, tag_status_zoom);
1570             break;
1571         }
1572         case Surge::Skin::Connector::NonParameterConnection::STORE_PATCH:
1573         {
1574             layoutComponentForSkin(skinCtrl, tag_store);
1575             break;
1576         }
1577         case Surge::Skin::Connector::NonParameterConnection::MSEG_EDITOR_OPEN:
1578         {
1579             msegEditSwitch = layoutComponentForSkin(skinCtrl, tag_mseg_edit);
1580             msegEditSwitch->setVisible(false);
1581             msegEditSwitch->setValue(isAnyOverlayPresent(MSEG_EDITOR));
1582             auto q = modsource_editor[current_scene];
1583             if ((q >= ms_lfo1 && q <= ms_lfo6) || (q >= ms_slfo1 && q <= ms_slfo6))
1584             {
1585                 auto *lfodata = &(synth->storage.getPatch().scene[current_scene].lfo[q - ms_lfo1]);
1586                 if (lfodata->shape.val.i == lt_mseg)
1587                     msegEditSwitch->setVisible(true);
1588             }
1589             break;
1590         }
1591         case Surge::Skin::Connector::NonParameterConnection::LFO_MENU:
1592         {
1593             layoutComponentForSkin(skinCtrl, tag_lfo_menu);
1594 
1595             break;
1596         }
1597         case Surge::Skin::Connector::NonParameterConnection::LFO_LABEL:
1598         {
1599             // Room for improvement, obviously
1600             lfoNameLabel = new CVerticalLabel(skinCtrl->getRect(), "");
1601             lfoNameLabel->setTransparency(true);
1602 #if !TARGET_JUCE_UI
1603             lfoNameLabel->setFont(Surge::GUI::getLatoAtSize(10, kBoldFace));
1604 #endif
1605             lfoNameLabel->setFontColor(currentSkin->getColor(Colors::LFO::Title::Text));
1606             lfoNameLabel->setHoriAlign(kCenterText);
1607             frame->addView(lfoNameLabel);
1608 
1609             break;
1610         }
1611 
1612         case Surge::Skin::Connector::NonParameterConnection::FXPRESET_LABEL:
1613         {
1614             // Room for improvement, obviously
1615             fxPresetLabel = new CTextLabel(skinCtrl->getRect(), "Preset");
1616             fxPresetLabel->setFontColor(currentSkin->getColor(Colors::Effect::Preset::Name));
1617             fxPresetLabel->setTransparency(true);
1618             fxPresetLabel->setFont(displayFont);
1619             fxPresetLabel->setHoriAlign(kRightText);
1620 #if !TARGET_JUCE_UI
1621             fxPresetLabel->setTextTruncateMode(CTextLabel::TextTruncateMode::kTruncateTail);
1622 #endif
1623             frame->addView(fxPresetLabel);
1624             break;
1625         }
1626         case Surge::Skin::Connector::NonParameterConnection::PATCH_BROWSER:
1627         {
1628             patchname =
1629                 new CPatchBrowser(skinCtrl->getRect(), this, tag_patchname, &synth->storage);
1630             ((CPatchBrowser *)patchname)->setSkin(currentSkin, bitmapStore);
1631             ((CPatchBrowser *)patchname)->setLabel(synth->storage.getPatch().name);
1632             ((CPatchBrowser *)patchname)->setCategory(synth->storage.getPatch().category);
1633             ((CPatchBrowser *)patchname)->setIDs(synth->current_category_id, synth->patchid);
1634             ((CPatchBrowser *)patchname)->setAuthor(synth->storage.getPatch().author);
1635             frame->addView(patchname);
1636             break;
1637         }
1638         case Surge::Skin::Connector::NonParameterConnection::FX_SELECTOR:
1639         {
1640             CEffectSettings *fc = new CEffectSettings(skinCtrl->getRect(), this, tag_fx_select,
1641                                                       current_fx, bitmapStore);
1642             fc->setSkin(currentSkin, bitmapStore);
1643             ccfxconf = fc;
1644             for (int i = 0; i < n_fx_slots; i++)
1645             {
1646                 fc->set_type(i, synth->storage.getPatch().fx[i].type.val.i);
1647             }
1648             fc->set_bypass(synth->storage.getPatch().fx_bypass.val.i);
1649             fc->set_disable(synth->storage.getPatch().fx_disable.val.i);
1650             frame->addView(fc);
1651             break;
1652         }
1653         case Surge::Skin::Connector::NonParameterConnection::MAIN_VU_METER:
1654         { // main vu-meter
1655             vu[0] = new CSurgeVuMeter(skinCtrl->getRect(), this);
1656             ((CSurgeVuMeter *)vu[0])->setSkin(currentSkin, bitmapStore);
1657             ((CSurgeVuMeter *)vu[0])->setType(vut_vu_stereo);
1658             frame->addView(vu[0]);
1659             break;
1660         }
1661         case Surge::Skin::Connector::NonParameterConnection::PARAMETER_CONNECTED:
1662         case Surge::Skin::Connector::NonParameterConnection::STORE_PATCH_DIALOG:
1663         case Surge::Skin::Connector::NonParameterConnection::MSEG_EDITOR_WINDOW:
1664         case Surge::Skin::Connector::NonParameterConnection::N_NONCONNECTED:
1665             break;
1666         }
1667     }
1668 
1669     memset(param, 0, n_paramslots * sizeof(void *));
1670     memset(nonmod_param, 0, n_paramslots * sizeof(void *));
1671     int i = 0;
1672     vector<Parameter *>::iterator iter;
1673     for (iter = synth->storage.getPatch().param_ptr.begin();
1674          iter != synth->storage.getPatch().param_ptr.end(); iter++)
1675     {
1676         if (i == n_paramslots)
1677         {
1678             // This would only happen if a dev added params.
1679             Surge::UserInteractions::promptError(
1680                 "INTERNAL ERROR: List of parameters is larger than maximum number of parameter "
1681                 "slots. Increase n_paramslots in SurgeGUIEditor.h!",
1682                 "Error");
1683         }
1684         Parameter *p = *iter;
1685         bool paramIsVisible = ((p->scene == (current_scene + 1)) || (p->scene == 0)) &&
1686                               isControlVisible(p->ctrlgroup, p->ctrlgroup_entry) &&
1687                               (p->ctrltype != ct_none);
1688 
1689         auto conn = Surge::Skin::Connector::connectorByID(p->ui_identifier);
1690         std::string uiid = p->ui_identifier;
1691 
1692         long style = p->ctrlstyle;
1693 
1694         if (p->ctrltype == ct_fmratio)
1695         {
1696             if (p->extend_range || p->absolute)
1697                 p->val_default.f = 16.f;
1698             else
1699                 p->val_default.f = 1.f;
1700         }
1701 
1702         if (p->hasSkinConnector &&
1703             conn.payload->defaultComponent != Surge::Skin::Components::None && paramIsVisible)
1704         {
1705             /*
1706              * Some special cases where we don't add a control
1707              */
1708             bool addControl = true;
1709 
1710             // Case: Analog envelopes have no shapers
1711             if (p->ctrltype == ct_envshape || p->ctrltype == ct_envshape_attack)
1712             {
1713                 addControl = (synth->storage.getPatch()
1714                                   .scene[current_scene]
1715                                   .adsr[p->ctrlgroup_entry]
1716                                   .mode.val.i == emt_digital);
1717             }
1718 
1719             if (addControl)
1720             {
1721                 auto skinCtrl = currentSkin->getOrCreateControlForConnector(conn);
1722                 currentSkin->resolveBaseParentOffsets(skinCtrl);
1723                 layoutComponentForSkin(skinCtrl, p->id + start_paramtags, i, p,
1724                                        style | conn.payload->controlStyleFlags);
1725 
1726                 uiidToSliderLabel[p->ui_identifier] = p->get_name();
1727                 if (p->id == synth->learn_param)
1728                 {
1729                     showMidiLearnOverlay(param[p->id]->getViewSize());
1730                 }
1731             }
1732         }
1733         i++;
1734     }
1735 
1736     // resonance link mode
1737     if (synth->storage.getPatch().scene[current_scene].f2_link_resonance.val.b)
1738     {
1739         i = synth->storage.getPatch().scene[current_scene].filterunit[1].resonance.id;
1740         if (param[i] && dynamic_cast<CSurgeSlider *>(param[i]) != nullptr)
1741             ((CSurgeSlider *)param[i])->disabled = true;
1742     }
1743 
1744     // feedback control
1745     if (synth->storage.getPatch().scene[current_scene].filterblock_configuration.val.i ==
1746         fc_serial1)
1747     {
1748         i = synth->storage.getPatch().scene[current_scene].feedback.id;
1749         if (param[i] && dynamic_cast<CSurgeSlider *>(param[i]) != nullptr)
1750         {
1751             bool curr = ((CSurgeSlider *)param[i])->disabled;
1752             ((CSurgeSlider *)param[i])->disabled = true;
1753             if (!curr)
1754             {
1755                 param[i]->setDirty();
1756                 param[i]->invalid();
1757             }
1758         }
1759     }
1760 
1761     // pan2 control
1762     if ((synth->storage.getPatch().scene[current_scene].filterblock_configuration.val.i !=
1763          fc_stereo) &&
1764         (synth->storage.getPatch().scene[current_scene].filterblock_configuration.val.i != fc_wide))
1765     {
1766         i = synth->storage.getPatch().scene[current_scene].width.id;
1767         if (param[i] && dynamic_cast<CSurgeSlider *>(param[i]) != nullptr)
1768         {
1769             bool curr = ((CSurgeSlider *)param[i])->disabled;
1770             ((CSurgeSlider *)param[i])->disabled = true;
1771             if (!curr)
1772             {
1773                 param[i]->setDirty();
1774                 param[i]->invalid();
1775             }
1776         }
1777     }
1778 
1779     int menuX = 844, menuY = 550, menuW = 50, menuH = 15;
1780     CRect menurect(menuX, menuY, menuX + menuW, menuY + menuH);
1781 
1782     infowindow = new CParameterTooltip(CRect(0, 0, 0, 0));
1783     ((CParameterTooltip *)infowindow)->setSkin(currentSkin);
1784     frame->addView(infowindow);
1785 
1786     // Mouse behavior
1787     if (CSurgeSlider::sliderMoveRateState == CSurgeSlider::kUnInitialized)
1788         CSurgeSlider::sliderMoveRateState =
1789             (CSurgeSlider::MoveRateState)Surge::Storage::getUserDefaultValue(
1790                 &(synth->storage), "sliderMoveRateState", (int)CSurgeSlider::kLegacy);
1791 
1792     /*
1793     ** Skin Labels
1794     */
1795     auto labels = currentSkin->getLabels();
1796 
1797     for (auto &l : labels)
1798     {
1799         auto mtext = currentSkin->propertyValue(l, Surge::Skin::Component::TEXT);
1800         auto ctext = currentSkin->propertyValue(l, Surge::Skin::Component::CONTROL_TEXT);
1801         if (ctext.isJust() && uiidToSliderLabel.find(ctext.fromJust()) != uiidToSliderLabel.end())
1802         {
1803 
1804             mtext = Surge::Maybe<std::string>(uiidToSliderLabel[ctext.fromJust()]);
1805         }
1806 
1807         if (mtext.isJust())
1808         {
1809             VSTGUI::CHoriTxtAlign txtalign = Surge::UI::Skin::setTextAlignProperty(
1810                 currentSkin->propertyValue(l, Surge::Skin::Component::TEXT_ALIGN, "left"));
1811 
1812             auto fs = currentSkin->propertyValue(l, Surge::Skin::Component::FONT_SIZE, "12");
1813             auto fsize = std::atof(fs.c_str());
1814 
1815             VSTGUI::CTxtFace fstyle = Surge::UI::Skin::setFontStyleProperty(
1816                 currentSkin->propertyValue(l, Surge::Skin::Component::FONT_STYLE, "normal"));
1817 
1818             auto coln =
1819                 currentSkin->propertyValue(l, Surge::Skin::Component::TEXT_COLOR, "#FF0000");
1820             auto col = currentSkin->getColor(coln, kRedCColor);
1821 
1822             auto dcol = VSTGUI::CColor(255, 255, 255, 0);
1823             auto bgcoln = currentSkin->propertyValue(l, Surge::Skin::Component::BACKGROUND_COLOR,
1824                                                      "#FFFFFF00");
1825             auto bgcol = currentSkin->getColor(bgcoln, dcol);
1826 
1827             auto frcoln =
1828                 currentSkin->propertyValue(l, Surge::Skin::Component::FRAME_COLOR, "#FFFFFF00");
1829             auto frcol = currentSkin->getColor(frcoln, dcol);
1830 
1831             auto lb = new CTextLabel(CRect(CPoint(l->x, l->y), CPoint(l->w, l->h)),
1832                                      mtext.fromJust().c_str());
1833             lb->setTransparency((bgcol == dcol && frcol == dcol));
1834             lb->setHoriAlign(txtalign);
1835 
1836 #if !TARGET_JUCE_UI
1837             lb->setAntialias(true);
1838             lb->setFont(Surge::GUI::getLatoAtSize(fsize, fstyle));
1839 #endif
1840 
1841             lb->setFontColor(col);
1842             lb->setBackColor(bgcol);
1843             lb->setFrameColor(frcol);
1844 
1845             frame->addView(lb);
1846         }
1847         else
1848         {
1849             auto image = currentSkin->propertyValue(l, Surge::Skin::Component::IMAGE);
1850             if (image.isJust())
1851             {
1852                 auto bmp = bitmapStore->getBitmapByStringID(image.fromJust());
1853                 if (bmp)
1854                 {
1855                     auto lb =
1856                         new CTextLabel(CRect(CPoint(l->x, l->y), CPoint(l->w, l->h)), nullptr, bmp);
1857                     frame->addView(lb);
1858                 }
1859             }
1860         }
1861     }
1862 #if BUILD_IS_DEBUG
1863     /*
1864      * This code is here JUST because baconpaul keeps developing surge and then swapping
1865      * to make music and wondering why LPX is stuttering. Please don't remove it!
1866      *
1867      * UPDATE: Might as well keep a reference to the object though so we can touch it in idle
1868      */
1869     auto dl = std::string("D ") + Surge::Build::BuildTime + " " + Surge::Build::GitBranch;
1870     auto lb = new CTextLabel(CRect(CPoint(310, 39), CPoint(195, 15)), dl.c_str());
1871     lb->setTransparency(false);
1872     lb->setBackColor(kRedCColor);
1873     lb->setFontColor(kWhiteCColor);
1874     lb->setFont(displayFont);
1875     lb->setHoriAlign(VSTGUI::kCenterText);
1876     lb->setAntialias(true);
1877     frame->addView(lb);
1878     debugLabel = lb;
1879 
1880 #if TARGET_JUCE_UI
1881     struct TestC : public juce::Component
1882     {
1883         ~TestC() { std::cout << "TestC Cleaned Up" << std::endl; }
1884         juce::Colour bg = juce::Colour(255, 0, 255);
1885         void paint(juce::Graphics &g) override
1886         {
1887             g.fillAll(bg);
1888             g.setColour(juce::Colour(255, 255, 255));
1889             g.drawText("JUCE BUILD", getLocalBounds(), juce::Justification::centred);
1890         }
1891         void mouseDown(const juce::MouseEvent &e) override
1892         {
1893             auto q = rand() % 255;
1894             bg = juce::Colour(255, q, 255 - q);
1895             repaint();
1896         }
1897     };
1898     auto pt = std::make_unique<TestC>();
1899     frame->juceComponent()->addAndMakeVisible(*pt);
1900     pt->setBounds(150, 2, 100, 10);
1901     frame->takeOwnership(std::move(pt));
1902 #endif
1903 #endif
1904     for (auto el : editorOverlay)
1905     {
1906         frame->addView(el.second);
1907         auto contents = editorOverlayContentsWeakReference[el.second];
1908 
1909         /*
1910          * This is a hack for 1.8 which we have to clean up in 1.9 when we do #3223
1911          */
1912         auto mse = dynamic_cast<RefreshableOverlay *>(contents);
1913         if (mse)
1914         {
1915             mse->forceRefresh();
1916         }
1917     }
1918 
1919     if (showMSEGEditorOnNextIdleOrOpen)
1920     {
1921         showMSEGEditor();
1922         showMSEGEditorOnNextIdleOrOpen = false;
1923     }
1924 
1925     // We need this here in case we rebuild when opening a new patch.
1926     // closeMSEGEditor does nothing if the mseg editor isn't open
1927     auto lfoidx = modsource_editor[current_scene] - ms_lfo1;
1928     if (lfoidx >= 0 && lfoidx <= n_lfos)
1929     {
1930         auto ld = &(synth->storage.getPatch().scene[current_scene].lfo[lfoidx]);
1931         if (ld->shape.val.i != lt_mseg)
1932         {
1933             closeMSEGEditor();
1934         }
1935     }
1936 
1937     refresh_mod();
1938 
1939     editor_open = true;
1940     queue_refresh = false;
1941 
1942     /* When we rebuild, the onMouseEntered event is not re-sent to the new component by VSTGui;
1943      * Maybe this is a bug in VSTG? But it screws up our hover states so force an onMouseEntered
1944      * if we are over a coponent
1945      */
1946     CPoint tr;
1947     frame->getCurrentMouseLocation(tr);
1948 
1949     // OK to *find* the component we need to look in transformed coordinates
1950     CPoint ttr = tr;
1951     frame->getTransform().transform(ttr);
1952     auto v = frame->getViewAt(ttr);
1953 
1954     if (v)
1955     {
1956         // but to *hover* the component we need to do it in 100-scale coordinates
1957         v->onMouseEntered(tr, 0);
1958     }
1959 
1960     frame->invalid();
1961 }
1962 
close_editor()1963 void SurgeGUIEditor::close_editor()
1964 {
1965     editor_open = false;
1966     lfodisplay = 0;
1967     frame->removeAll(true);
1968     setzero(param);
1969 }
1970 
1971 #if !TARGET_VST3
open(void * parent)1972 bool SurgeGUIEditor::open(void *parent)
1973 #else
1974 bool PLUGIN_API SurgeGUIEditor::open(void *parent, const PlatformType &platformType)
1975 #endif
1976 {
1977     if (samplerate == 0)
1978     {
1979         std::cout << "Sample rate was not set when editor opened. Defaulting to 44.1k" << std::endl;
1980 
1981         /*
1982         ** The oscillator displays need a sample rate; some test hosts don't call
1983         ** setSampleRate so if we are in this state make the bad but reasonable
1984         ** default choice
1985         */
1986         synth->setSamplerate(44100);
1987     }
1988 #if !TARGET_VST3
1989     // !!! always call this !!!
1990     super::open(parent);
1991 
1992 #if !TARGET_JUCE_UI
1993     PlatformType platformType = kDefaultNative;
1994 #else
1995     int platformType = 0;
1996 #endif
1997 #endif
1998 
1999 #if TARGET_VST3
2000 #if LINUX
2001     Steinberg::Linux::IRunLoop *l = nullptr;
2002     if (getIPlugFrame()->queryInterface(Steinberg::Linux::IRunLoop::iid, (void **)&l) ==
2003         Steinberg::kResultOk)
2004     {
2005         std::cout << "IPlugFrame is a runloop " << l << std::endl;
2006     }
2007     else
2008     {
2009         std::cout << "IPlugFrame is not a runloop " << l << std::endl;
2010     }
2011     if (l == nullptr)
2012     {
2013         Surge::UserInteractions::promptError("Starting VST3 with null runloop", "Software Error");
2014     }
2015     LinuxVST3Init(l);
2016 #endif
2017 #endif
2018 
2019     float fzf = getZoomFactor() / 100.0;
2020 #if TARGET_VST2
2021     CRect wsize(0, 0, currentSkin->getWindowSizeX() * fzf, currentSkin->getWindowSizeY() * fzf);
2022 #else
2023     CRect wsize(0, 0, currentSkin->getWindowSizeX(), currentSkin->getWindowSizeY());
2024 #endif
2025 
2026     CFrame *nframe = new CFrame(wsize, this);
2027 
2028 #if TARGET_JUCE_UI
2029     nframe->remember();
2030 #endif
2031 
2032     bitmapStore.reset(new SurgeBitmaps());
2033     bitmapStore->setupBitmapsForFrame(nframe);
2034     currentSkin->reloadSkin(bitmapStore);
2035     nframe->setZoom(fzf);
2036     /*
2037      * OK so WTF is this? Well:
2038      * CFrame is a CView so it can be a drop thing
2039      * but CFrame is final so you can't subclass it
2040      * You could call setDropTarget on it but that's not what CViewContainers use
2041      * They instead use this attribute, kCViewContainerDropTargetAttribute
2042      * but that's not public so copy its value here
2043      * and add a comment
2044      * and think - sigh. VSTGUI.
2045      */
2046 
2047 #if !TARGET_JUCE_UI
2048     dropAdapter = new SGEDropAdapter(this);
2049     dropAdapter->remember();
2050 
2051     VSTGUI::IDropTarget *dt = nullptr;
2052     nframe->getAttribute('vcdt', dt);
2053     if (dt)
2054         dt->forget();
2055     nframe->setAttribute('vcdt', dropAdapter);
2056 #endif
2057 
2058     frame = nframe;
2059 
2060 #if LINUX && TARGET_VST3
2061     LinuxVST3FrameOpen(frame, parent, platformType);
2062 #elif LINUX && TARGET_LV2
2063     VSTGUI::X11::FrameConfig frameConfig;
2064     frameConfig.runLoop = SurgeLv2::createRunLoop(_userdata);
2065     frame->open(parent, platformType, &frameConfig);
2066 #else
2067     frame->open(parent, platformType);
2068 #endif
2069 
2070 #if TARGET_VST3 || TARGET_LV2
2071     _idleTimer = VSTGUI::SharedPointer<VSTGUI::CVSTGUITimer>(
2072         new CVSTGUITimer([this](CVSTGUITimer *timer) { idle(); }, 50, false), false);
2073     _idleTimer->start();
2074 #endif
2075 
2076 #if TARGET_VST3 && LINUX
2077     firstIdleCountdown = 2;
2078 #endif
2079 
2080     /*#if TARGET_AUDIOUNIT
2081       synth = (sub3_synth*)_effect;
2082       #elif TARGET_VST3
2083       //synth = (sub3_synth*)_effect; ??
2084       #else
2085       vstlayer *plug = (vstlayer*)_effect;
2086       if(!plug->initialized) plug->init();
2087       synth = (sub3_synth*)plug->plugin_instance;
2088       #endif*/
2089 
2090     /*
2091     ** Register only once (when we open)
2092     */
2093 #if !TARGET_JUCE_UI
2094     frame->registerKeyboardHook(this);
2095 #endif
2096     reloadFromSkin();
2097     openOrRecreateEditor();
2098 
2099     if (getZoomFactor() != 100)
2100     {
2101         zoom_callback(this, true);
2102         zoomInvalid = true;
2103     }
2104 
2105     // HEREHERE
2106     auto *des = &(synth->storage.getPatch().dawExtraState);
2107 
2108     if (des->isPopulated && des->editor.isMSEGOpen)
2109         showMSEGEditor();
2110 
2111     return true;
2112 }
2113 
close()2114 void SurgeGUIEditor::close()
2115 {
2116     populateDawExtraState(synth);
2117     for (auto el : editorOverlay)
2118     {
2119         frame->removeView(el.second);
2120         auto f = editorOverlayOnClose[el.second];
2121         f();
2122         editorOverlayTagAtClose = el.first;
2123     }
2124 #if TARGET_VST2 // && WINDOWS
2125     // We may need this in other hosts also; but for now
2126     if (frame)
2127     {
2128         getFrame()->unregisterKeyboardHook(this);
2129         frame->close();
2130         frame = nullptr;
2131     }
2132 
2133     if (dropAdapter)
2134     {
2135         dropAdapter->buddy = nullptr;
2136         dropAdapter->forget();
2137         dropAdapter = nullptr;
2138     }
2139 
2140 #endif
2141 
2142 #if !TARGET_VST3
2143     super::close();
2144 #endif
2145 
2146 #if TARGET_VST3 || TARGET_LV2
2147     _idleTimer->stop();
2148     _idleTimer = nullptr;
2149 #endif
2150     hasIdleRun = false;
2151     firstIdleCountdown = 0;
2152 
2153 #if TARGET_VST3
2154 #if LINUX
2155     LinuxVST3Detatch();
2156 #endif
2157 #endif
2158 }
2159 
setParameter(long index,float value)2160 void SurgeGUIEditor::setParameter(long index, float value)
2161 {
2162     if (!frame)
2163         return;
2164     if (!editor_open)
2165         return;
2166     if (index > synth->storage.getPatch().param_ptr.size())
2167         return;
2168 
2169     // if(param[index])
2170     {
2171         // param[index]->setValue(value);
2172         int j = 0;
2173         while (j < 7)
2174         {
2175             if ((synth->refresh_ctrl_queue[j] > -1) && (synth->refresh_ctrl_queue[j] != index))
2176                 j++;
2177             else
2178                 break;
2179         }
2180         synth->refresh_ctrl_queue[j] = index;
2181         synth->refresh_ctrl_queue_value[j] = value;
2182     }
2183 }
2184 
decode_controllerid(char * txt,int id)2185 void decode_controllerid(char *txt, int id)
2186 {
2187     int type = (id & 0xffff0000) >> 16;
2188     int num = (id & 0xffff);
2189     switch (type)
2190     {
2191     case 1:
2192         snprintf(txt, TXT_SIZE, "NRPN %i", num);
2193         break;
2194     case 2:
2195         snprintf(txt, TXT_SIZE, "RPN %i", num);
2196         break;
2197     default:
2198         snprintf(txt, TXT_SIZE, "CC %i", num);
2199         break;
2200     };
2201 }
2202 
controlModifierClicked(CControl * control,CButtonState button)2203 int32_t SurgeGUIEditor::controlModifierClicked(CControl *control, CButtonState button)
2204 {
2205     if (!synth)
2206         return 0;
2207     if (!frame)
2208         return 0;
2209     if (!editor_open)
2210         return 0;
2211 
2212     if (button & (kMButton | kButton4 | kButton5))
2213     {
2214         toggle_mod_editing();
2215         return 1;
2216     }
2217 
2218     long tag = control->getTag();
2219 
2220     if ((button & kControl) && (tag == tag_mp_patch || tag == tag_mp_category))
2221     {
2222         synth->selectRandomPatch();
2223         return 1;
2224     }
2225 
2226     // In these cases just move along with success. RMB does nothing on these switches
2227     if (tag == tag_mp_jogfx || tag == tag_mp_category || tag == tag_mp_patch || tag == tag_store ||
2228         tag == tag_store_cancel || tag == tag_store_ok)
2229         return 1;
2230 
2231     if (tag == tag_lfo_menu)
2232     {
2233         control->setValue(0);
2234         CPoint where;
2235         frame->getCurrentMouseLocation(where);
2236         frame->localToFrame(where);
2237 
2238         showLfoMenu(where);
2239         return 1;
2240     }
2241 
2242     if (tag == tag_status_zoom)
2243     {
2244         CPoint where;
2245         frame->getCurrentMouseLocation(where);
2246         frame->localToFrame(where);
2247 
2248         showZoomMenu(where);
2249         return 1;
2250     }
2251     if (tag == tag_status_mpe)
2252     {
2253         CPoint where;
2254         frame->getCurrentMouseLocation(where);
2255         frame->localToFrame(where);
2256 
2257         showMPEMenu(where);
2258         return 1;
2259     }
2260     if (tag == tag_status_tune)
2261     {
2262         CPoint where;
2263         frame->getCurrentMouseLocation(where);
2264         frame->localToFrame(where);
2265 
2266         showTuningMenu(where);
2267         return 1;
2268     }
2269 
2270     std::vector<std::string> clearControlTargetNames;
2271 
2272     auto cmensl = dynamic_cast<CMenuAsSlider *>(control);
2273 
2274     if (cmensl && cmensl->deactivated)
2275         return 0;
2276 
2277     if (button & kRButton)
2278     {
2279         if (tag == tag_settingsmenu)
2280         {
2281             CRect r = control->getViewSize();
2282             CRect menuRect;
2283             CPoint where;
2284             frame->getCurrentMouseLocation(where);
2285             frame->localToFrame(where);
2286 
2287             menuRect.offset(where.x, where.y);
2288 
2289             useDevMenu = true;
2290             showSettingsMenu(menuRect);
2291             return 1;
2292         }
2293 
2294         if (tag == tag_osc_select)
2295         {
2296             CRect r = control->getViewSize();
2297             CRect menuRect;
2298 
2299             CPoint where;
2300             frame->getCurrentMouseLocation(where);
2301             frame->localToFrame(where);
2302 
2303             int a = limit_range((int)((3 * (where.x - r.left)) / r.getWidth()), 0, 2);
2304             menuRect.offset(where.x, where.y);
2305 
2306             COptionMenu *contextMenu =
2307                 new COptionMenu(menuRect, 0, 0, 0, 0, VSTGUI::COptionMenu::kNoDrawStyle);
2308             int eid = 0;
2309 
2310             char txt[256];
2311             auto hu = helpURLForSpecial("osc-select");
2312             if (hu != "")
2313             {
2314                 snprintf(txt, TXT_SIZE, "[?] Osc %i", a + 1);
2315                 auto lurl = fullyResolvedHelpURL(hu);
2316                 addCallbackMenu(contextMenu, txt,
2317                                 [lurl]() { Surge::UserInteractions::openURL(lurl); });
2318                 eid++;
2319             }
2320             else
2321             {
2322                 snprintf(txt, TXT_SIZE, "Osc %i", a + 1);
2323                 contextMenu->addEntry(txt, eid++);
2324             }
2325 
2326             contextMenu->addSeparator(eid++);
2327             addCallbackMenu(contextMenu, "Copy", [this, a]() {
2328                 synth->storage.clipboard_copy(cp_osc, current_scene, a);
2329             });
2330             eid++;
2331 
2332             addCallbackMenu(
2333                 contextMenu, Surge::UI::toOSCaseForMenu("Copy With Modulation"),
2334                 [this, a]() { synth->storage.clipboard_copy(cp_oscmod, current_scene, a); });
2335             eid++;
2336 
2337             if (synth->storage.get_clipboard_type() == cp_osc)
2338             {
2339                 addCallbackMenu(contextMenu, "Paste", [this, a]() {
2340                     synth->clear_osc_modulation(current_scene, a);
2341                     synth->storage.clipboard_paste(cp_osc, current_scene, a);
2342                     queue_refresh = true;
2343                 });
2344                 eid++;
2345             }
2346 
2347             frame->addView(contextMenu); // add to frame
2348             contextMenu->setDirty();
2349             contextMenu->popup();
2350             frame->removeView(contextMenu, true); // remove from frame and forget
2351 
2352             return 1;
2353         }
2354 
2355         if (tag == tag_scene_select)
2356         {
2357             CRect r = control->getViewSize();
2358             CRect menuRect;
2359             CPoint where;
2360             frame->getCurrentMouseLocation(where);
2361             frame->localToFrame(where);
2362 
2363             // Assume vertical alignment of the scene buttons
2364             int a = limit_range((int)((2 * (where.y - r.top)) / r.getHeight()), 0, n_scenes);
2365 
2366             if (auto aschsw = dynamic_cast<CHSwitch2 *>(control))
2367             {
2368                 if (aschsw->columns == n_scenes)
2369                 {
2370                     // We are horizontal due to skins or whatever so
2371                     a = limit_range((int)((2 * (where.x - r.left)) / r.getWidth()), 0, n_scenes);
2372                 }
2373             }
2374 
2375             menuRect.offset(where.x, where.y);
2376 
2377             COptionMenu *contextMenu =
2378                 new COptionMenu(menuRect, 0, 0, 0, 0, VSTGUI::COptionMenu::kNoDrawStyle);
2379             int eid = 0;
2380 
2381             char txt[256];
2382             auto hu = helpURLForSpecial("scene-select");
2383             if (hu != "")
2384             {
2385                 snprintf(txt, TXT_SIZE, "[?] Scene %c", 'A' + a);
2386                 auto lurl = fullyResolvedHelpURL(hu);
2387                 addCallbackMenu(contextMenu, txt,
2388                                 [lurl]() { Surge::UserInteractions::openURL(lurl); });
2389                 eid++;
2390             }
2391             else
2392             {
2393                 snprintf(txt, TXT_SIZE, "Scene %c", 'A' + a);
2394                 contextMenu->addEntry(txt, eid++);
2395             }
2396 
2397             contextMenu->addSeparator(eid++);
2398 
2399             addCallbackMenu(contextMenu, "Copy",
2400                             [this, a]() { synth->storage.clipboard_copy(cp_scene, a, -1); });
2401             eid++;
2402             if (synth->storage.get_clipboard_type() == cp_scene)
2403             {
2404                 addCallbackMenu(contextMenu, "Paste", [this, a]() {
2405                     synth->storage.clipboard_paste(cp_scene, a, -1);
2406                     queue_refresh = true;
2407                 });
2408                 eid++;
2409             }
2410 
2411             frame->addView(contextMenu); // add to frame
2412             contextMenu->setDirty();
2413             contextMenu->popup();
2414             frame->removeView(contextMenu, true); // remove from frame and forget
2415 
2416             return 1;
2417         }
2418     }
2419 
2420     if ((tag >= tag_mod_source0) && (tag < tag_mod_source_end))
2421     {
2422         modsources modsource = (modsources)(tag - tag_mod_source0);
2423 
2424         if (button & kRButton)
2425         {
2426             CModulationSourceButton *cms = (CModulationSourceButton *)control;
2427             CRect menuRect;
2428             CPoint where;
2429             frame->getCurrentMouseLocation(where);
2430             frame->localToFrame(where);
2431 
2432             menuRect.offset(where.x, where.y);
2433             COptionMenu *contextMenu = new COptionMenu(
2434                 menuRect, 0, 0, 0, 0,
2435                 VSTGUI::COptionMenu::kNoDrawStyle | VSTGUI::COptionMenu::kMultipleCheckStyle);
2436             int eid = 0;
2437 
2438             std::string hu;
2439             if (modsource >= ms_ctrl1 && modsource <= ms_ctrl8)
2440             {
2441                 hu = helpURLForSpecial("macro-modbutton");
2442             }
2443             else if (modsource >= ms_lfo1 && modsource <= ms_slfo6)
2444             {
2445                 hu = helpURLForSpecial("lfo-modbutton");
2446             }
2447             else if ((modsource >= ms_ampeg && modsource <= ms_filtereg) ||
2448                      (modsource >= ms_random_bipolar && modsource <= ms_alternate_unipolar))
2449             {
2450                 hu = helpURLForSpecial("internalmod-modbutton");
2451             }
2452 
2453             else
2454             {
2455                 hu = helpURLForSpecial("other-modbutton");
2456             }
2457 
2458             if (cms->hasAlternate)
2459             {
2460                 int idOn = modsource;
2461                 int idOff = cms->alternateId;
2462                 if (cms->useAlternate)
2463                 {
2464                     auto t = idOn;
2465                     idOn = idOff;
2466                     idOff = t;
2467                 }
2468 
2469                 if (hu != "")
2470                 {
2471                     auto lurl = fullyResolvedHelpURL(hu);
2472                     std::string hs = std::string("[?] ") + (char *)modsource_names[idOn];
2473                     addCallbackMenu(contextMenu, hs,
2474                                     [lurl]() { Surge::UserInteractions::openURL(lurl); });
2475                     eid++;
2476                 }
2477                 else
2478                 {
2479                     contextMenu->addEntry((char *)modsource_names[idOn], eid++);
2480                 }
2481                 std::string offLab = "Switch to ";
2482                 offLab += modsource_names[idOff];
2483                 bool activeMod = (cms->state & 3) == 2;
2484 
2485                 auto *mi = addCallbackMenu(contextMenu, offLab, [this, modsource, cms]() {
2486                     cms->setUseAlternate(!cms->useAlternate);
2487                     modsource_is_alternate[modsource] = cms->useAlternate;
2488                     this->refresh_mod();
2489                 });
2490                 if (activeMod)
2491                     mi->setEnabled(false);
2492                 eid++;
2493             }
2494             else
2495             {
2496                 if (hu != "")
2497                 {
2498                     auto lurl = fullyResolvedHelpURL(hu);
2499                     std::string hs = std::string("[?] ") + modulatorName(modsource, false);
2500                     addCallbackMenu(contextMenu, hs,
2501                                     [lurl]() { Surge::UserInteractions::openURL(lurl); });
2502                     eid++;
2503                 }
2504                 else
2505                 {
2506                     contextMenu->addEntry((char *)modulatorName(modsource, false).c_str(), eid++);
2507                 }
2508             }
2509 
2510             int n_total_md = synth->storage.getPatch().param_ptr.size();
2511 
2512             const int max_md = 4096;
2513             assert(max_md >= n_total_md);
2514 
2515             bool cancellearn = false;
2516             int ccid = 0;
2517 
2518             int detailedMode = Surge::Storage::getUserDefaultValue(&(this->synth->storage),
2519                                                                    "highPrecisionReadouts", 0);
2520 
2521             // should start at 0, but started at 1 before.. might be a reason but don't remember
2522             // why...
2523             std::vector<modsources> possibleSources;
2524             possibleSources.push_back(modsource);
2525             if (cms->hasAlternate)
2526             {
2527                 possibleSources.push_back((modsources)(cms->alternateId));
2528             }
2529 
2530             for (auto thisms : possibleSources)
2531             {
2532                 bool first_destination = true;
2533                 int n_md = 0;
2534 
2535                 for (int md = 0; md < n_total_md; md++)
2536                 {
2537                     auto activeScene = synth->storage.getPatch().scene_active.val.i;
2538                     Parameter *parameter = synth->storage.getPatch().param_ptr[md];
2539 
2540                     if (((md < n_global_params) || ((parameter->scene - 1) == activeScene)) &&
2541                         synth->isActiveModulation(md, thisms))
2542                     {
2543                         char modtxt[TXT_SIZE];
2544                         auto pmd = synth->storage.getPatch().param_ptr[md];
2545                         pmd->get_display_of_modulation_depth(modtxt, synth->getModDepth(md, thisms),
2546                                                              synth->isBipolarModulation(thisms),
2547                                                              Parameter::Menu);
2548                         char tmptxt[1024]; // leave room for that ubuntu 20.0 error
2549                         if (pmd->ctrlgroup == cg_LFO)
2550                         {
2551                             char pname[TXT_SIZE];
2552                             pmd->create_fullname(pmd->get_name(), pname, pmd->ctrlgroup,
2553                                                  pmd->ctrlgroup_entry,
2554                                                  modulatorName(pmd->ctrlgroup_entry, true).c_str());
2555                             snprintf(tmptxt, TXT_SIZE, "Edit %s -> %s: %s",
2556                                      (char *)modulatorName(thisms, true).c_str(), pname, modtxt);
2557                         }
2558                         else
2559                         {
2560                             snprintf(tmptxt, TXT_SIZE, "Edit %s -> %s: %s",
2561                                      (char *)modulatorName(thisms, true).c_str(),
2562                                      pmd->get_full_name(), modtxt);
2563                         }
2564 
2565                         auto clearOp = [this, parameter, control, thisms]() {
2566                             this->promptForUserValueEntry(parameter, control, thisms);
2567                         };
2568 
2569                         if (first_destination)
2570                         {
2571                             contextMenu->addSeparator(eid++);
2572                             first_destination = false;
2573                         }
2574 
2575                         addCallbackMenu(contextMenu, tmptxt, clearOp);
2576                         eid++;
2577                     }
2578                 }
2579 
2580                 first_destination = true;
2581                 for (int md = 0; md < n_total_md; md++)
2582                 {
2583                     auto activeScene = synth->storage.getPatch().scene_active.val.i;
2584                     Parameter *parameter = synth->storage.getPatch().param_ptr[md];
2585 
2586                     if (((md < n_global_params) || ((parameter->scene - 1) == activeScene)) &&
2587                         synth->isActiveModulation(md, thisms))
2588                     {
2589                         char tmptxt[1024];
2590                         if (parameter->ctrlgroup == cg_LFO)
2591                         {
2592                             char pname[512];
2593                             parameter->create_fullname(
2594                                 parameter->get_name(), pname, parameter->ctrlgroup,
2595                                 parameter->ctrlgroup_entry,
2596                                 modulatorName(parameter->ctrlgroup_entry, true).c_str());
2597                             snprintf(tmptxt, TXT_SIZE, "Clear %s -> %s",
2598                                      (char *)modulatorName(thisms, true).c_str(), pname);
2599                         }
2600                         else
2601                         {
2602                             snprintf(tmptxt, TXT_SIZE, "Clear %s -> %s",
2603                                      (char *)modulatorName(thisms, true).c_str(),
2604                                      parameter->get_full_name());
2605                         }
2606 
2607                         auto clearOp = [this, first_destination, md, n_total_md, thisms,
2608                                         control]() {
2609                             bool resetName = false;   // Should I reset the name?
2610                             std::string newName = ""; // And to what?
2611                             int ccid = thisms - ms_ctrl1;
2612 
2613                             if (first_destination)
2614                             {
2615                                 if (strncmp(synth->storage.getPatch().CustomControllerLabel[ccid],
2616                                             synth->storage.getPatch().param_ptr[md]->get_name(),
2617                                             15) == 0)
2618                                 {
2619                                     // So my modulator is named after my short name. I haven't been
2620                                     // renamed. So I want to reset at least to "-" unless someone is
2621                                     // after me
2622                                     resetName = true;
2623                                     newName = "-";
2624 
2625                                     // Now we have to find if there's another modulation below me
2626                                     int nextmd = md + 1;
2627                                     while (nextmd < n_total_md &&
2628                                            !synth->isActiveModulation(nextmd, thisms))
2629                                         nextmd++;
2630                                     if (nextmd < n_total_md && strlen(synth->storage.getPatch()
2631                                                                           .param_ptr[nextmd]
2632                                                                           ->get_name()) > 1)
2633                                         newName =
2634                                             synth->storage.getPatch().param_ptr[nextmd]->get_name();
2635                                 }
2636                             }
2637 
2638                             synth->clearModulation(md, thisms);
2639                             refresh_mod();
2640                             control->setDirty();
2641                             control->invalid();
2642 
2643                             if (resetName)
2644                             {
2645                                 // And this is where we apply the name refresh, of course.
2646                                 strxcpy(synth->storage.getPatch().CustomControllerLabel[ccid],
2647                                         newName.c_str(), 15);
2648                                 synth->storage.getPatch().CustomControllerLabel[ccid][15] = 0;
2649                                 ((CModulationSourceButton *)control)
2650                                     ->setlabel(
2651                                         synth->storage.getPatch().CustomControllerLabel[ccid]);
2652                                 control->setDirty();
2653                                 control->invalid();
2654                                 synth->updateDisplay();
2655                             }
2656                         };
2657 
2658                         if (first_destination)
2659                         {
2660                             contextMenu->addSeparator(eid++);
2661                             first_destination = false;
2662                         }
2663 
2664                         addCallbackMenu(contextMenu, tmptxt, clearOp);
2665                         eid++;
2666 
2667                         n_md++;
2668                     }
2669                 }
2670                 if (n_md)
2671                 {
2672                     std::string clearLab;
2673                     std::string modName;
2674 
2675                     // hacky, but works - we want to retain the capitalization for modulator names
2676                     // regardless of OS!
2677                     modName = modulatorName(thisms, false);
2678                     clearLab = Surge::UI::toOSCaseForMenu("Clear All ") + modName +
2679                                Surge::UI::toOSCaseForMenu(" Routings");
2680 
2681                     addCallbackMenu(contextMenu, clearLab, [this, n_total_md, thisms, control]() {
2682                         for (int md = 1; md < n_total_md; md++)
2683                             synth->clearModulation(md, thisms);
2684                         refresh_mod();
2685 
2686                         // Also blank out the name and rebuild the UI
2687                         if (within_range(ms_ctrl1, thisms, ms_ctrl1 + n_customcontrollers - 1))
2688                         {
2689                             int ccid = thisms - ms_ctrl1;
2690 
2691                             synth->storage.getPatch().CustomControllerLabel[ccid][0] = '-';
2692                             synth->storage.getPatch().CustomControllerLabel[ccid][1] = 0;
2693                             ((CModulationSourceButton *)control)
2694                                 ->setlabel(synth->storage.getPatch().CustomControllerLabel[ccid]);
2695                         }
2696                         control->setDirty(true);
2697                         control->invalid();
2698                         synth->updateDisplay();
2699                     });
2700                     eid++;
2701                 }
2702             }
2703             int sc = limit_range(synth->storage.getPatch().scene_active.val.i, 0, n_scenes - 1);
2704 #if TARGET_VST3
2705             Steinberg::Vst::IContextMenu *hostMenu = nullptr;
2706 #endif
2707 
2708             if (within_range(ms_ctrl1, modsource, ms_ctrl1 + n_customcontrollers - 1))
2709             {
2710                 /*
2711                 ** This is the menu for the controls
2712                 */
2713 
2714                 ccid = modsource - ms_ctrl1;
2715 
2716                 auto cms = ((ControllerModulationSource *)synth->storage.getPatch()
2717                                 .scene[current_scene]
2718                                 .modsources[modsource]);
2719 
2720                 contextMenu->addSeparator(eid++);
2721                 char vtxt[1024];
2722                 snprintf(vtxt, 1024, "%s: %.*f %%",
2723                          Surge::UI::toOSCaseForMenu("Edit Value").c_str(), (detailedMode ? 6 : 2),
2724                          100 * cms->get_output());
2725                 addCallbackMenu(contextMenu, vtxt, [this, control, modsource]() {
2726                     promptForUserValueEntry(nullptr, control, modsource);
2727                 });
2728                 eid++;
2729 
2730                 contextMenu->addSeparator(eid++);
2731 
2732                 char txt[TXT_SIZE];
2733 
2734                 // Construct submenus for explicit controller mapping
2735                 COptionMenu *midiSub = new COptionMenu(
2736                     menuRect, 0, 0, 0, 0,
2737                     VSTGUI::COptionMenu::kNoDrawStyle | VSTGUI::COptionMenu::kMultipleCheckStyle);
2738                 COptionMenu *currentSub = nullptr;
2739 
2740                 for (int subs = 0; subs < 7; ++subs)
2741                 {
2742                     if (currentSub)
2743                     {
2744                         currentSub->forget();
2745                         currentSub = nullptr;
2746                     }
2747                     currentSub = new COptionMenu(menuRect, 0, 0, 0, 0,
2748                                                  VSTGUI::COptionMenu::kNoDrawStyle |
2749                                                      VSTGUI::COptionMenu::kMultipleCheckStyle);
2750 
2751                     char name[16];
2752 
2753                     snprintf(name, 16, "CC %d ... %d", subs * 20, min((subs * 20) + 20, 128) - 1);
2754 
2755                     auto added_to_menu = midiSub->addEntry(currentSub, name);
2756 
2757                     for (int item = 0; item < 20; ++item)
2758                     {
2759                         int mc = (subs * 20) + item;
2760                         int disabled = 0;
2761 
2762                         // these CCs cannot be used for MIDI learn (see
2763                         // SurgeSynthesizer::channelController)
2764                         if (mc == 0 || mc == 6 || mc == 32 || mc == 38 || mc == 64 ||
2765                             (mc == 74 && synth->mpeEnabled) || (mc >= 98 && mc <= 101) ||
2766                             mc == 120 || mc == 123)
2767                             disabled = 1;
2768 
2769                         // we don't have any more CCs to cover, so break the loop
2770                         if (mc > 127)
2771                             break;
2772 
2773                         char name[128];
2774 
2775                         snprintf(name, 128, "CC %d (%s) %s", mc, midicc_names[mc],
2776                                  (disabled == 1 ? "- RESERVED" : ""));
2777 
2778                         CCommandMenuItem *cmd = new CCommandMenuItem(CCommandMenuItem::Desc(name));
2779 
2780                         cmd->setActions([this, ccid, mc](CCommandMenuItem *men) {
2781                             synth->storage.controllers[ccid] = mc;
2782                         });
2783                         cmd->setEnabled(!disabled);
2784 
2785                         auto added = currentSub->addEntry(cmd);
2786 
2787                         if (synth->storage.controllers[ccid] == mc)
2788                         {
2789                             added->setChecked();
2790                             added_to_menu->setChecked();
2791                         }
2792                     }
2793                 }
2794 
2795                 if (currentSub)
2796                 {
2797                     currentSub->forget();
2798                     currentSub = nullptr;
2799                 }
2800 
2801                 contextMenu->addEntry(midiSub, Surge::UI::toOSCaseForMenu("Assign Macro To..."));
2802 
2803                 eid++;
2804 
2805                 if (synth->learn_custom > -1 && synth->learn_custom == ccid)
2806                     cancellearn = true;
2807 
2808                 std::string learnTag =
2809                     cancellearn ? "Abort Macro MIDI Learn" : "MIDI Learn Macro...";
2810                 addCallbackMenu(contextMenu, Surge::UI::toOSCaseForMenu(learnTag),
2811                                 [this, cancellearn, control, ccid] {
2812                                     if (cancellearn)
2813                                     {
2814                                         hideMidiLearnOverlay();
2815                                         synth->learn_custom = -1;
2816                                     }
2817                                     else
2818                                     {
2819                                         showMidiLearnOverlay(control->getViewSize());
2820                                         synth->learn_custom = ccid;
2821                                     }
2822                                 });
2823                 eid++;
2824 
2825                 if (synth->storage.controllers[ccid] >= 0)
2826                 {
2827                     char txt4[16];
2828                     decode_controllerid(txt4, synth->storage.controllers[ccid]);
2829                     snprintf(txt, TXT_SIZE, "Clear Learned MIDI (%s ", txt4);
2830                     addCallbackMenu(contextMenu,
2831                                     Surge::UI::toOSCaseForMenu(txt) +
2832                                         midicc_names[synth->storage.controllers[ccid]] + ")",
2833                                     [this, ccid]() { synth->storage.controllers[ccid] = -1; });
2834                     eid++;
2835                 }
2836 
2837                 contextMenu->addSeparator(eid++);
2838 
2839                 addCallbackMenu(contextMenu, Surge::UI::toOSCaseForMenu("Bipolar Mode"),
2840                                 [this, control, ccid]() {
2841                                     bool bp = !synth->storage.getPatch()
2842                                                    .scene[current_scene]
2843                                                    .modsources[ms_ctrl1 + ccid]
2844                                                    ->is_bipolar();
2845                                     synth->storage.getPatch()
2846                                         .scene[current_scene]
2847                                         .modsources[ms_ctrl1 + ccid]
2848                                         ->set_bipolar(bp);
2849 
2850                                     float f = synth->storage.getPatch()
2851                                                   .scene[current_scene]
2852                                                   .modsources[ms_ctrl1 + ccid]
2853                                                   ->get_output01();
2854                                     control->setValue(f);
2855                                     ((CModulationSourceButton *)control)->setBipolar(bp);
2856                                     refresh_mod();
2857                                 });
2858                 contextMenu->checkEntry(eid, synth->storage.getPatch()
2859                                                  .scene[current_scene]
2860                                                  .modsources[ms_ctrl1 + ccid]
2861                                                  ->is_bipolar());
2862                 eid++;
2863 
2864                 addCallbackMenu(
2865                     contextMenu, Surge::UI::toOSCaseForMenu("Rename Macro..."),
2866                     [this, control, ccid, menuRect]() {
2867                         std::string pval = synth->storage.getPatch().CustomControllerLabel[ccid];
2868                         if (pval == "-")
2869                             pval = "";
2870                         promptForMiniEdit(
2871                             pval, "Enter a new name for the macro:", "Rename Macro",
2872                             menuRect.getTopLeft(), [this, control, ccid](const std::string &s) {
2873                                 auto useS = s;
2874                                 if (useS == "")
2875                                     useS = "-";
2876                                 strxcpy(synth->storage.getPatch().CustomControllerLabel[ccid],
2877                                         useS.c_str(), 16);
2878                                 synth->storage.getPatch().CustomControllerLabel[ccid][15] =
2879                                     0; // to be sure
2880                                 ((CModulationSourceButton *)control)
2881                                     ->setlabel(
2882                                         synth->storage.getPatch().CustomControllerLabel[ccid]);
2883 
2884                                 control->setDirty();
2885                                 control->invalid();
2886                                 synth->refresh_editor = true;
2887                                 // synth->updateDisplay();
2888                             });
2889                     });
2890                 eid++;
2891 
2892 #if TARGET_VST3
2893                 SurgeSynthesizer::ID mid;
2894                 if (synth->fromSynthSideId(modsource - ms_ctrl1 + metaparam_offset, mid))
2895                     hostMenu = addVst3MenuForParams(contextMenu, mid, eid);
2896 #endif
2897 
2898                 midiSub->forget();
2899             }
2900 
2901             int lfo_id = isLFO(modsource) ? modsource - ms_lfo1 : -1;
2902 
2903             if (lfo_id >= 0)
2904             {
2905                 contextMenu->addSeparator(eid++);
2906                 addCallbackMenu(contextMenu, "Copy", [this, sc, lfo_id]() {
2907                     if (lfo_id >= 0)
2908                         synth->storage.clipboard_copy(cp_lfo, sc, lfo_id);
2909                     mostRecentCopiedMSEGState = msegEditState[sc][lfo_id];
2910                 });
2911                 eid++;
2912 
2913                 if (synth->storage.get_clipboard_type() == cp_lfo)
2914                 {
2915                     addCallbackMenu(contextMenu, "Paste", [this, sc, lfo_id]() {
2916                         if (lfo_id >= 0)
2917                             synth->storage.clipboard_paste(cp_lfo, sc, lfo_id);
2918                         msegEditState[sc][lfo_id] = mostRecentCopiedMSEGState;
2919                         queue_refresh = true;
2920                     });
2921                     eid++;
2922                 }
2923             }
2924             frame->addView(contextMenu); // add to frame
2925             contextMenu->setDirty();
2926             contextMenu->popup();
2927             frame->removeView(contextMenu, true); // remove from frame and forget
2928 
2929 #if TARGET_VST3
2930             if (hostMenu)
2931                 hostMenu->release();
2932 #endif
2933 
2934             return 1;
2935         }
2936         return 0;
2937     }
2938 
2939     if (tag < start_paramtags)
2940         return 0;
2941 
2942     if (!(button & (kDoubleClick | kRButton | kControl)))
2943         return 0;
2944 
2945     int ptag = tag - start_paramtags;
2946 
2947     if ((ptag >= 0) && (ptag < synth->storage.getPatch().param_ptr.size()))
2948     {
2949         Parameter *p = synth->storage.getPatch().param_ptr[ptag];
2950 
2951         // don't show RMB context menu for filter subtype if it's hidden/not applicable
2952         auto f1type = synth->storage.getPatch().scene[current_scene].filterunit[0].type.val.i;
2953         auto f2type = synth->storage.getPatch().scene[current_scene].filterunit[1].type.val.i;
2954 
2955         if (tag == f1subtypetag && (f1type == fut_none || f1type == fut_SNH))
2956             return 1;
2957         if (tag == f2subtypetag && (f2type == fut_none || f2type == fut_SNH))
2958             return 1;
2959 
2960         bool blockForLFO = false;
2961         if (p->ctrltype == ct_lfotype)
2962         {
2963             blockForLFO = true;
2964             auto *clfo = dynamic_cast<CLFOGui *>(control);
2965             if (clfo)
2966             {
2967                 CPoint where;
2968                 frame->getCurrentMouseLocation(where);
2969                 frame->localToFrame(where);
2970 
2971                 blockForLFO = !clfo->insideTypeSelector(where);
2972             }
2973         }
2974 
2975         // all the RMB context menus
2976         if ((button & kRButton) && !blockForLFO)
2977         {
2978             CRect menuRect;
2979             CPoint where;
2980             frame->getCurrentMouseLocation(where);
2981             frame->localToFrame(where);
2982 
2983             menuRect.offset(where.x, where.y);
2984 
2985             COptionMenu *contextMenu = new COptionMenu(
2986                 menuRect, 0, 0, 0, 0,
2987                 VSTGUI::COptionMenu::kNoDrawStyle | VSTGUI::COptionMenu::kMultipleCheckStyle);
2988             int eid = 0;
2989 
2990             // FIXME - only fail at this once
2991             /* if( bitmapStore->getBitmapByPath( synth->storage.datapath +
2992             "skins/shared/help-24.svg" ) == nullptr )
2993             {
2994                std::cout << "LOADING BMP" << std::endl;
2995                bitmapStore->loadBitmapByPath( synth->storage.datapath + "skins/shared/help-24.svg"
2996             );
2997             }
2998             auto helpbmp = bitmapStore->getBitmapByPath( synth->storage.datapath +
2999             "skins/shared/help-24.svg" ); std::cout << "HELPBMP is " << helpbmp << std::endl; */
3000             // auto pp = IPlatformBitmap::createFromPath( (synth->storage.datapath +
3001             // "skins/shared/help-14.png" ).c_str() ); auto helpbmp = new CBitmap( pp );
3002 
3003             std::string helpurl = helpURLFor(p);
3004 
3005             if (helpurl == "")
3006             {
3007                 contextMenu->addEntry((char *)p->get_full_name(), eid++);
3008             }
3009             else
3010             {
3011                 std::string helpstr = "[?] ";
3012                 auto lurl = fullyResolvedHelpURL(helpurl);
3013                 addCallbackMenu(contextMenu, std::string(helpstr + p->get_full_name()).c_str(),
3014                                 [lurl]() { Surge::UserInteractions::openURL(lurl); });
3015                 eid++;
3016             }
3017 
3018             contextMenu->addSeparator(eid++);
3019 
3020             char txt[TXT_SIZE], txt2[512];
3021             p->get_display(txt);
3022             snprintf(txt2, 512, "%s: %s", Surge::UI::toOSCaseForMenu("Edit Value").c_str(), txt);
3023 
3024             if (p->valtype == vt_float)
3025             {
3026                 if (p->can_temposync() && p->temposync)
3027                 {
3028                     COptionMenu *tsMenuR =
3029                         new COptionMenu(menuRect, 0, 0, 0, 0,
3030                                         VSTGUI::COptionMenu::kNoDrawStyle |
3031                                             VSTGUI::COptionMenu::kMultipleCheckStyle);
3032                     COptionMenu *tsMenuD =
3033                         new COptionMenu(menuRect, 0, 0, 0, 0,
3034                                         VSTGUI::COptionMenu::kNoDrawStyle |
3035                                             VSTGUI::COptionMenu::kMultipleCheckStyle);
3036                     COptionMenu *tsMenuT =
3037                         new COptionMenu(menuRect, 0, 0, 0, 0,
3038                                         VSTGUI::COptionMenu::kNoDrawStyle |
3039                                             VSTGUI::COptionMenu::kMultipleCheckStyle);
3040 
3041                     // have 0 1 2 then 0 + log2 1.5 1 + log2 1.5 then 0 + log2 1.33 and so on
3042                     for (int i = p->val_min.f; i <= p->val_max.f; ++i)
3043                     {
3044                         float mul = 1.0;
3045                         float triplaboff = log2(1.33333333);
3046                         float tripoff = triplaboff;
3047                         float dotlaboff = log2(1.5);
3048                         float dotoff = dotlaboff;
3049                         if (p->ctrltype == ct_lforate || p->ctrltype == ct_lforate_deactivatable)
3050                         {
3051                             mul = -1.0;
3052                             triplaboff = log2(1.5);
3053                             tripoff = triplaboff;
3054                             dotlaboff = log2(1.3333333333);
3055                             dotoff = dotlaboff;
3056                         }
3057                         addCallbackMenu(tsMenuR, p->tempoSyncNotationValue(mul * ((float)i)),
3058                                         [p, i, this]() {
3059                                             p->val.f = (float)i;
3060                                             p->bound_value();
3061                                             this->synth->refresh_editor = true;
3062                                         });
3063                         addCallbackMenu(tsMenuD,
3064                                         p->tempoSyncNotationValue(mul * ((float)i + dotlaboff)),
3065                                         [p, i, dotoff, this]() {
3066                                             p->val.f = (float)i + dotoff;
3067                                             p->bound_value();
3068                                             this->synth->refresh_editor = true;
3069                                         });
3070                         addCallbackMenu(tsMenuT,
3071                                         p->tempoSyncNotationValue(mul * ((float)i + triplaboff)),
3072                                         [p, i, tripoff, this]() {
3073                                             p->val.f = (float)i + tripoff;
3074                                             p->bound_value();
3075                                             this->synth->refresh_editor = true;
3076                                         });
3077                     }
3078                     contextMenu->addEntry(txt2);
3079                     eid++;
3080                     contextMenu->addSeparator();
3081                     eid++;
3082                     contextMenu->addEntry(tsMenuR, "Straight");
3083                     tsMenuR->forget();
3084                     eid++;
3085                     contextMenu->addEntry(tsMenuD, "Dotted");
3086                     tsMenuD->forget();
3087                     eid++;
3088                     contextMenu->addEntry(tsMenuT, "Triplet");
3089                     tsMenuT->forget();
3090                     eid++;
3091                     contextMenu->addSeparator();
3092                     eid++;
3093                 }
3094                 else
3095                 {
3096                     addCallbackMenu(contextMenu, txt2, [this, p, control]() {
3097                         this->promptForUserValueEntry(p, control);
3098                     });
3099                     eid++;
3100                 }
3101             }
3102             else if (p->valtype == vt_bool)
3103             {
3104                 // FIXME - make this a checked toggle
3105                 auto b = addCallbackMenu(contextMenu, txt2, [this, p, control]() {
3106                     SurgeSynthesizer::ID pid;
3107                     if (synth->fromSynthSideId(p->id, pid))
3108                     {
3109                         synth->setParameter01(pid, !p->val.b, false, false);
3110                         repushAutomationFor(p);
3111                         synth->refresh_editor = true;
3112                     }
3113                 });
3114 
3115                 eid++;
3116 
3117                 if (p->val.b)
3118                 {
3119                     b->setChecked(true);
3120                 }
3121 
3122                 if (p->ctrltype == ct_bool_keytrack || p->ctrltype == ct_bool_retrigger)
3123                 {
3124                     std::vector<Parameter *> impactedParms;
3125                     std::vector<std::string> tgltxt = {"Enable", "Disable"};
3126                     std::string parname = "Keytrack";
3127                     if (p->ctrltype == ct_bool_retrigger)
3128                     {
3129                         parname = "Retrigger";
3130                     }
3131 
3132                     int currvals = 0;
3133                     // There is surely a more efficient way but this is fine
3134                     for (auto iter = this->synth->storage.getPatch().param_ptr.begin();
3135                          iter != this->synth->storage.getPatch().param_ptr.end(); iter++)
3136                     {
3137                         Parameter *pl = *iter;
3138                         if (pl->ctrltype == p->ctrltype && pl->scene == p->scene)
3139                         {
3140                             currvals += pl->val.b;
3141                             impactedParms.push_back(pl);
3142                         }
3143                     }
3144 
3145                     int ktsw = (currvals == n_oscs);
3146 
3147                     auto txt3 = Surge::UI::toOSCaseForMenu(tgltxt[ktsw] + " " + parname +
3148                                                            " for All Oscillators");
3149 
3150                     auto b =
3151                         addCallbackMenu(contextMenu, txt3.c_str(), [this, ktsw, impactedParms]() {
3152                             for (auto *p : impactedParms)
3153                             {
3154                                 SurgeSynthesizer::ID pid;
3155 
3156                                 if (synth->fromSynthSideId(p->id, pid))
3157                                 {
3158                                     synth->setParameter01(pid, 1 - ktsw, false, false);
3159                                     repushAutomationFor(p);
3160                                 }
3161                             }
3162 
3163                             synth->refresh_editor = true;
3164                         });
3165 
3166                     eid++;
3167                 }
3168             }
3169             else if (p->valtype == vt_int)
3170             {
3171                 if (p->can_setvalue_from_string())
3172                 {
3173                     addCallbackMenu(contextMenu, txt2, [this, p, control]() {
3174                         this->promptForUserValueEntry(p, control);
3175                     });
3176                     eid++;
3177                 }
3178                 else
3179                 {
3180                     int incr = 1;
3181 
3182                     if (p->ctrltype == ct_vocoder_bandcount)
3183                         incr = 4;
3184 
3185                     // we have a case where the number of menu entries to be generated depends on
3186                     // another parameter so instead of using val_max.i directly, store it to local
3187                     // var and modify its value when required
3188                     int max = p->val_max.i;
3189 
3190                     // currently we only have this case with filter subtypes - different filter
3191                     // types have a different number of them so let's do this!
3192                     bool isCombOnSubtype = false;
3193                     if (p->ctrltype == ct_filtersubtype)
3194                     {
3195                         auto ftype = synth->storage.getPatch()
3196                                          .scene[current_scene]
3197                                          .filterunit[p->ctrlgroup_entry]
3198                                          .type.val.i;
3199                         if (ftype == fut_comb_pos || ftype == fut_comb_neg)
3200                             isCombOnSubtype = true;
3201                         max = fut_subcount[ftype] - 1;
3202                     }
3203 
3204                     auto dm = dynamic_cast<ParameterDiscreteIndexRemapper *>(p->user_data);
3205                     if (dm)
3206                     {
3207                         std::unordered_map<std::string, std::map<int, int>> reorderMap;
3208                         std::vector<std::string> groupAppearanceOrder;
3209                         for (int i = p->val_min.i; i <= max; i += incr)
3210                         {
3211                             int idx = dm->remapStreamedIndexToDisplayIndex(i);
3212                             if (idx >= 0)
3213                             {
3214                                 auto gn = dm->groupNameAtStreamedIndex(i);
3215                                 if (reorderMap.find(gn) == reorderMap.end())
3216                                 {
3217                                     groupAppearanceOrder.push_back(gn);
3218                                 }
3219 
3220                                 reorderMap[gn][idx] = i;
3221                             }
3222                         }
3223 
3224                         if (dm->sortGroupNames())
3225                             std::sort(groupAppearanceOrder.begin(), groupAppearanceOrder.end());
3226                         else if (dm->useRemappedOrderingForGroupsIfNotSorted())
3227                         {
3228                             groupAppearanceOrder.clear();
3229                             std::map<int, std::string> gnr;
3230                             for (int i = p->val_min.i; i <= max; i += incr)
3231                             {
3232                                 int idx = dm->remapStreamedIndexToDisplayIndex(i);
3233                                 if (idx >= 0)
3234                                 {
3235                                     gnr[idx] = dm->groupNameAtStreamedIndex(i);
3236                                 }
3237                             }
3238                             std::unordered_set<std::string> dealt;
3239                             for (const auto &p : gnr)
3240                             {
3241                                 if (dealt.find(p.second) == dealt.end())
3242                                 {
3243                                     groupAppearanceOrder.push_back(p.second);
3244                                     dealt.insert(p.second);
3245                                 }
3246                             }
3247                         }
3248 
3249                         bool useSubMenus = dm->hasGroupNames();
3250                         COptionMenu *sub = nullptr;
3251                         for (auto grpN : groupAppearanceOrder)
3252                         {
3253                             auto grp = reorderMap[grpN];
3254                             bool checkSub = false;
3255                             if (useSubMenus)
3256                             {
3257                                 sub = new COptionMenu(menuRect, 0, 0, 0, 0,
3258                                                       VSTGUI::COptionMenu::kNoDrawStyle |
3259                                                           VSTGUI::COptionMenu::kMultipleCheckStyle);
3260                             }
3261                             for (auto rp : grp)
3262                             {
3263                                 int i = rp.second;
3264                                 std::string displaytxt = dm->nameAtStreamedIndex(i);
3265                                 auto addTo = useSubMenus ? sub : contextMenu;
3266                                 if (useSubMenus && grpN == "")
3267                                     addTo = contextMenu;
3268 
3269 #if WINDOWS
3270                                 Surge::Storage::findReplaceSubstring(displaytxt, std::string("&"),
3271                                                                      std::string("&&"));
3272 #endif
3273 
3274                                 auto b =
3275                                     addCallbackMenu(addTo, displaytxt.c_str(), [this, p, i, tag]() {
3276                                         float ef = Parameter::intScaledToFloat(i, p->val_max.i,
3277                                                                                p->val_min.i);
3278                                         synth->setParameter01(synth->idForParameter(p), ef, false,
3279                                                               false);
3280                                         repushAutomationFor(p);
3281                                         synth->refresh_editor = true;
3282                                     });
3283                                 eid++;
3284                                 if (i == p->val.i)
3285                                 {
3286                                     b->setChecked(true);
3287                                     checkSub = true;
3288                                 }
3289                             }
3290                             if (useSubMenus && grpN != "")
3291                             {
3292                                 auto e = contextMenu->addEntry(sub, grpN.c_str());
3293                                 sub->forget();
3294                                 sub = nullptr;
3295                                 e->setChecked(checkSub);
3296                             }
3297                         }
3298                     }
3299                     else
3300                     {
3301                         for (int i = p->val_min.i; i <= max; i += incr)
3302                         {
3303                             char txt[256];
3304                             float ef = (1.0f * i - p->val_min.i) / (p->val_max.i - p->val_min.i);
3305                             p->get_display(txt, true, ef);
3306 
3307                             std::string displaytxt = txt;
3308 
3309 #if WINDOWS
3310                             Surge::Storage::findReplaceSubstring(displaytxt, std::string("&"),
3311                                                                  std::string("&&"));
3312 #endif
3313 
3314                             auto b = addCallbackMenu(
3315                                 contextMenu, displaytxt.c_str(), [this, ef, p, i]() {
3316                                     synth->setParameter01(synth->idForParameter(p), ef, false,
3317                                                           false);
3318                                     repushAutomationFor(p);
3319                                     synth->refresh_editor = true;
3320                                 });
3321                             eid++;
3322                             if (i == p->val.i)
3323                                 b->setChecked(true);
3324                         }
3325                         if (isCombOnSubtype)
3326                         {
3327                             contextMenu->addSeparator();
3328                             auto m = addCallbackMenu(
3329                                 contextMenu, Surge::UI::toOSCaseForMenu("Precise Tuning"),
3330                                 [this]() {
3331                                     synth->storage.getPatch().correctlyTuneCombFilter =
3332                                         !synth->storage.getPatch().correctlyTuneCombFilter;
3333                                 });
3334                             m->setChecked(synth->storage.getPatch().correctlyTuneCombFilter);
3335                         }
3336 
3337                         if (p->ctrltype == ct_polymode &&
3338                             (p->val.i == pm_mono || p->val.i == pm_mono_st ||
3339                              p->val.i == pm_mono_fp || p->val.i == pm_mono_st_fp))
3340                         {
3341                             std::vector<std::string> labels = {"Last", "High", "Low", "Legacy"};
3342                             std::vector<MonoVoicePriorityMode> vals = {
3343                                 ALWAYS_LATEST, ALWAYS_HIGHEST, ALWAYS_LOWEST,
3344                                 NOTE_ON_LATEST_RETRIGGER_HIGHEST};
3345                             contextMenu->addSeparator();
3346                             for (int i = 0; i < 4; ++i)
3347                             {
3348                                 auto m = addCallbackMenu(
3349                                     contextMenu,
3350                                     Surge::UI::toOSCaseForMenu(labels[i] + " Note Priority"),
3351                                     [this, vals, i]() {
3352                                         synth->storage.getPatch()
3353                                             .scene[current_scene]
3354                                             .monoVoicePriorityMode = vals[i];
3355                                     });
3356                                 if (vals[i] == synth->storage.getPatch()
3357                                                    .scene[current_scene]
3358                                                    .monoVoicePriorityMode)
3359                                     m->setChecked(true);
3360                             }
3361                             contextMenu->addSeparator();
3362                             contextMenu->addEntry(
3363                                 makeMonoModeOptionsMenu(menuRect, false),
3364                                 Surge::UI::toOSCaseForMenu("Sustain Pedal In Mono Mode"));
3365                         }
3366                     }
3367                 }
3368             }
3369             bool cancellearn = false;
3370 
3371             // Modulation and Learn semantics only apply to vt_float types in Surge right now
3372             if (p->valtype == vt_float)
3373             {
3374                 // if(p->can_temposync() || p->can_extend_range()) contextMenu->addSeparator(eid++);
3375                 if (p->can_temposync())
3376                 {
3377                     auto r = addCallbackMenu(contextMenu, Surge::UI::toOSCaseForMenu("Tempo Sync"),
3378                                              [this, p, control]() {
3379                                                  p->temposync = !p->temposync;
3380                                                  if (p->temposync)
3381                                                      p->bound_value();
3382                                                  else if (control)
3383                                                      p->set_value_f01(control->getValue());
3384 
3385                                                  if (this->lfodisplay)
3386                                                      this->lfodisplay->invalid();
3387                                                  auto *css = dynamic_cast<CSurgeSlider *>(control);
3388                                                  if (css)
3389                                                  {
3390                                                      css->setTempoSync(p->temposync);
3391                                                      css->invalid();
3392                                                  }
3393                                              });
3394                     if (p->temposync)
3395                         r->setChecked(true);
3396                     eid++;
3397 
3398                     if (p->ctrlgroup == cg_ENV || p->ctrlgroup == cg_LFO)
3399                     {
3400                         char label[256];
3401                         char prefix[128];
3402                         char pars[32];
3403 
3404                         int a = p->ctrlgroup_entry;
3405 
3406                         if (p->ctrlgroup == cg_ENV)
3407                         {
3408                             if (a == 0)
3409                                 sprintf(prefix, "Amp EG");
3410                             else
3411                                 sprintf(prefix, "Filter EG");
3412                         }
3413                         else if (p->ctrlgroup == cg_LFO)
3414                         {
3415                             if (a >= ms_lfo1 && a <= ms_lfo1 + n_lfos_voice)
3416                                 sprintf(prefix, "Voice LFO %i", a - ms_lfo1 + 1);
3417                             else if (a >= ms_slfo1 && a <= ms_slfo1 + n_lfos_scene)
3418                                 sprintf(prefix, "Scene LFO %i", a - ms_slfo1 + 1);
3419                         }
3420 
3421                         bool setTSTo;
3422 
3423 #if WINDOWS
3424                         snprintf(pars, 32, "parameters");
3425 #else
3426                         snprintf(pars, 32, "Parameters");
3427 #endif
3428 
3429                         if (p->temposync)
3430                         {
3431                             snprintf(
3432                                 label, 256, "%s %s %s",
3433                                 Surge::UI::toOSCaseForMenu("Disable Tempo Sync for All").c_str(),
3434                                 prefix, pars);
3435                             setTSTo = false;
3436                         }
3437                         else
3438                         {
3439                             snprintf(
3440                                 label, 256, "%s %s %s",
3441                                 Surge::UI::toOSCaseForMenu("Enable Tempo Sync for All").c_str(),
3442                                 prefix, pars);
3443                             setTSTo = true;
3444                         }
3445 
3446                         addCallbackMenu(contextMenu, label, [this, p, setTSTo]() {
3447                             // There is surely a more efficient way but this is fine
3448                             for (auto iter = this->synth->storage.getPatch().param_ptr.begin();
3449                                  iter != this->synth->storage.getPatch().param_ptr.end(); iter++)
3450                             {
3451                                 Parameter *pl = *iter;
3452                                 if (pl->ctrlgroup_entry == p->ctrlgroup_entry &&
3453                                     pl->ctrlgroup == p->ctrlgroup && pl->can_temposync())
3454                                 {
3455                                     pl->temposync = setTSTo;
3456                                     if (setTSTo)
3457                                         pl->bound_value();
3458                                 }
3459                             }
3460                             this->synth->refresh_editor = true;
3461                         });
3462                         eid++;
3463                     }
3464                 }
3465 
3466                 switch (p->ctrltype)
3467                 {
3468                 case ct_freq_audible_with_tunability:
3469                 case ct_freq_audible_with_very_low_lowerbound:
3470                     addCallbackMenu(
3471                         contextMenu,
3472                         Surge::UI::toOSCaseForMenu("Reset Filter Cutoff To Keytrack Root"),
3473                         [this, p, control] {
3474                             auto kr = this->synth->storage.getPatch()
3475                                           .scene[current_scene]
3476                                           .keytrack_root.val.i;
3477                             p->set_value_f01(p->value_to_normalized(kr - 69));
3478                             control->setValue(p->get_value_f01());
3479                         });
3480                     eid++;
3481                     break;
3482 
3483                 default:
3484                     break;
3485                 }
3486 
3487                 if (p->has_portaoptions())
3488                 {
3489                     contextMenu->addSeparator(eid++);
3490 
3491                     addCallbackMenu(contextMenu, Surge::UI::toOSCaseForMenu("Constant Rate"),
3492                                     [this, p]() { p->porta_constrate = !p->porta_constrate; });
3493                     contextMenu->checkEntry(eid, p->porta_constrate);
3494                     eid++;
3495 
3496                     addCallbackMenu(contextMenu, Surge::UI::toOSCaseForMenu("Glissando"),
3497                                     [this, p]() { p->porta_gliss = !p->porta_gliss; });
3498                     contextMenu->checkEntry(eid, p->porta_gliss);
3499                     eid++;
3500 
3501                     addCallbackMenu(contextMenu,
3502                                     Surge::UI::toOSCaseForMenu("Retrigger at Scale Degrees"),
3503                                     [this, p]() { p->porta_retrigger = !p->porta_retrigger; });
3504                     contextMenu->checkEntry(eid, p->porta_retrigger);
3505                     eid++;
3506 
3507                     contextMenu->addSeparator(eid++);
3508 
3509                     addCallbackMenu(contextMenu, Surge::UI::toOSCaseForMenu("Logarithmic Curve"),
3510                                     [this, p]() { p->porta_curve = -1; });
3511                     contextMenu->checkEntry(eid, (p->porta_curve == -1));
3512                     eid++;
3513 
3514                     addCallbackMenu(contextMenu, Surge::UI::toOSCaseForMenu("Linear Curve"),
3515                                     [this, p]() { p->porta_curve = 0; });
3516                     contextMenu->checkEntry(eid, (p->porta_curve == 0));
3517                     eid++;
3518 
3519                     addCallbackMenu(contextMenu, Surge::UI::toOSCaseForMenu("Exponential Curve"),
3520                                     [this, p]() { p->porta_curve = 1; });
3521                     contextMenu->checkEntry(eid, (p->porta_curve == 1));
3522                     eid++;
3523                 }
3524 
3525                 if (p->has_deformoptions())
3526                 {
3527                     switch (p->ctrltype)
3528                     {
3529 
3530                     case ct_lfodeform:
3531                     {
3532                         auto q = modsource_editor[current_scene];
3533                         auto *lfodata =
3534                             &(synth->storage.getPatch().scene[current_scene].lfo[q - ms_lfo1]);
3535 
3536                         if (lt_num_deforms[lfodata->shape.val.i] > 1)
3537                         {
3538                             contextMenu->addSeparator(eid++);
3539 
3540                             for (int i = 0; i < lt_num_deforms[lfodata->shape.val.i]; i++)
3541                             {
3542                                 char title[32];
3543                                 sprintf(title, "Deform Type %d", (i + 1));
3544 
3545                                 addCallbackMenu(contextMenu, Surge::UI::toOSCaseForMenu(title),
3546                                                 [this, p, i]() {
3547                                                     p->deform_type = i;
3548                                                     if (frame)
3549                                                         frame->invalid();
3550                                                 });
3551                                 contextMenu->checkEntry(eid, (p->deform_type == i));
3552                                 eid++;
3553                             }
3554                         }
3555 
3556                         break;
3557                     }
3558                     case ct_modern_trimix:
3559                     {
3560                         contextMenu->addSeparator();
3561                         eid++;
3562 
3563                         std::vector<int> waves = {ModernOscillator::mo_multitypes::momt_triangle,
3564                                                   ModernOscillator::mo_multitypes::momt_sine,
3565                                                   ModernOscillator::mo_multitypes::momt_square};
3566 
3567                         for (int m : waves)
3568                         {
3569                             auto mtm =
3570                                 addCallbackMenu(contextMenu, mo_multitype_names[m], [p, m, this]() {
3571                                     // p->deform_type = m;
3572                                     p->deform_type = (p->deform_type & 0xFFF0) | m;
3573                                     synth->refresh_editor = true;
3574                                 });
3575 
3576                             mtm->setChecked((p->deform_type & 0x0F) == m);
3577                             eid++;
3578                         }
3579 
3580                         contextMenu->addSeparator();
3581                         eid++;
3582 
3583                         int subosc = ModernOscillator::mo_submask::mo_subone;
3584 
3585                         auto mtm = addCallbackMenu(
3586                             contextMenu, Surge::UI::toOSCaseForMenu("Sub-oscillator Mode"),
3587                             [p, subosc, this]() {
3588                                 auto usubosc = subosc;
3589                                 int usubskipsync =
3590                                     p->deform_type & ModernOscillator::mo_submask::mo_subskipsync;
3591 
3592                                 if (p->deform_type & subosc)
3593                                 {
3594                                     usubosc = 0;
3595                                 }
3596 
3597                                 p->deform_type = (p->deform_type & 0xF) | usubosc | usubskipsync;
3598 
3599                                 synth->refresh_editor = true;
3600                             });
3601 
3602                         mtm->setChecked((p->deform_type & subosc));
3603                         eid++;
3604 
3605                         int subskipsync = ModernOscillator::mo_submask::mo_subskipsync;
3606 
3607                         auto skp = addCallbackMenu(
3608                             contextMenu,
3609                             Surge::UI::toOSCaseForMenu("Disable Hardsync in Sub-oscillator Mode"),
3610                             [p, subskipsync, this]() {
3611                                 auto usubskipsync = subskipsync;
3612                                 int usubosc =
3613                                     p->deform_type & ModernOscillator::mo_submask::mo_subone;
3614 
3615                                 if (p->deform_type & subskipsync)
3616                                 {
3617                                     usubskipsync = 0;
3618                                 }
3619 
3620                                 p->deform_type = (p->deform_type & 0xF) | usubosc | usubskipsync;
3621 
3622                                 synth->refresh_editor = true;
3623                             });
3624 
3625                         skp->setChecked((p->deform_type & subskipsync));
3626                         eid++;
3627 
3628                         break;
3629                     }
3630                     case ct_alias_mask:
3631                     {
3632                         contextMenu->addSeparator();
3633                         eid++;
3634 
3635                         auto trimask = addCallbackMenu(
3636                             contextMenu,
3637                             Surge::UI::toOSCaseForMenu("Triangle not masked after threshold point"),
3638                             [p, this]() {
3639                                 p->deform_type = !p->deform_type;
3640 
3641                                 synth->refresh_editor = true;
3642                             });
3643 
3644                         trimask->setChecked(p->deform_type);
3645                         eid++;
3646 
3647                         break;
3648                     }
3649                     default:
3650                     {
3651                         break;
3652                     }
3653                     }
3654                 }
3655 
3656                 if (p->can_extend_range())
3657                 {
3658                     if (!(p->ctrltype == ct_fmratio && p->can_be_absolute() && p->absolute))
3659                     {
3660                         bool enable = true;
3661                         bool visible = true;
3662                         std::string txt = "Extend Range";
3663                         switch (p->ctrltype)
3664                         {
3665                         case ct_reson_res_extendable:
3666                             txt = "Modulation Extends into Self-oscillation";
3667                             break;
3668                         case ct_freq_audible_with_tunability:
3669                         case ct_freq_audible_with_very_low_lowerbound:
3670                             txt = "Apply SCL/KBM Tuning to Filter Cutoff";
3671                             visible =
3672                                 synth->storage.tuningApplicationMode == SurgeStorage::RETUNE_ALL;
3673                             break;
3674                         case ct_percent_oscdrift:
3675                             txt = "Randomize Initial Drift Phase";
3676                             break;
3677                         case ct_twist_aux_mix:
3678                             txt = "Pan Main and Auxilliary Signals";
3679                             break;
3680                         default:
3681                             break;
3682                         }
3683 
3684                         if (visible)
3685                         {
3686                             auto ee = addCallbackMenu(contextMenu, Surge::UI::toOSCaseForMenu(txt),
3687                                                       [this, p]() {
3688                                                           p->extend_range = !p->extend_range;
3689                                                           this->synth->refresh_editor = true;
3690                                                       });
3691                             contextMenu->checkEntry(eid, p->extend_range);
3692                             ee->setEnabled(enable);
3693                             eid++;
3694                         }
3695                     }
3696                 }
3697 
3698                 if (p->can_be_absolute())
3699                 {
3700                     addCallbackMenu(contextMenu, "Absolute", [this, p]() {
3701                         p->absolute = !p->absolute;
3702 
3703                         // FIXME : What's a better aprpoach?
3704                         if (p->ctrltype == ct_fmratio)
3705                         {
3706                             char txt[256], ntxt[256];
3707                             memset(txt, 0, 256);
3708                             strxcpy(txt, p->get_name(), 256);
3709                             if (p->absolute)
3710                             {
3711                                 snprintf(ntxt, 256, "M%c Frequency", txt[1]);
3712                             }
3713                             else
3714                             {
3715                                 snprintf(ntxt, 256, "M%c Ratio",
3716                                          txt[1]); // Ladies and gentlemen, MC Ratio!
3717                             }
3718                             p->set_name(ntxt);
3719                             synth->refresh_editor = true;
3720                         }
3721                     });
3722                     contextMenu->checkEntry(eid, p->absolute);
3723                     eid++;
3724                 }
3725 
3726                 if (p->can_deactivate() || p->get_primary_deactivation_driver())
3727                 {
3728                     auto q = p->get_primary_deactivation_driver();
3729                     if (!q)
3730                     {
3731                         q = p;
3732                     }
3733 
3734                     std::string txt;
3735 
3736                     if (q->deactivated)
3737                     {
3738                         if (q->ctrltype == ct_envtime_linkable_delay)
3739                         {
3740                             txt = Surge::UI::toOSCaseForMenu("Unlink from Left Channel");
3741                         }
3742                         else
3743                         {
3744                             txt = Surge::UI::toOSCaseForMenu("Activate");
3745                         }
3746 
3747                         addCallbackMenu(contextMenu, txt.c_str(), [this, q]() {
3748                             q->deactivated = false;
3749                             this->synth->refresh_editor = true;
3750                         });
3751                     }
3752                     else
3753                     {
3754                         if (q->ctrltype == ct_envtime_linkable_delay)
3755                         {
3756                             txt = Surge::UI::toOSCaseForMenu("Link to Left Channel");
3757                         }
3758                         else
3759                         {
3760                             txt = Surge::UI::toOSCaseForMenu("Deactivate");
3761                         }
3762 
3763                         addCallbackMenu(contextMenu, txt.c_str(), [this, q]() {
3764                             q->deactivated = true;
3765                             this->synth->refresh_editor = true;
3766                         });
3767                     }
3768 
3769                     eid++;
3770                 }
3771 
3772                 contextMenu->addSeparator(eid++);
3773 
3774                 // Construct submenus for explicit controller mapping
3775                 COptionMenu *midiSub = new COptionMenu(
3776                     menuRect, 0, 0, 0, 0,
3777                     VSTGUI::COptionMenu::kNoDrawStyle | VSTGUI::COptionMenu::kMultipleCheckStyle);
3778                 COptionMenu *currentSub = nullptr;
3779 
3780                 for (int subs = 0; subs < 7; ++subs)
3781                 {
3782                     if (currentSub)
3783                     {
3784                         currentSub->forget();
3785                         currentSub = nullptr;
3786                     }
3787                     currentSub = new COptionMenu(menuRect, 0, 0, 0, 0,
3788                                                  VSTGUI::COptionMenu::kNoDrawStyle |
3789                                                      VSTGUI::COptionMenu::kMultipleCheckStyle);
3790 
3791                     char name[16];
3792 
3793                     sprintf(name, "CC %d ... %d", subs * 20, min((subs * 20) + 20, 128) - 1);
3794 
3795                     auto added_to_menu = midiSub->addEntry(currentSub, name);
3796 
3797                     for (int item = 0; item < 20; ++item)
3798                     {
3799                         int mc = (subs * 20) + item;
3800                         int disabled = 0;
3801 
3802                         // these CCs cannot be used for MIDI learn (see
3803                         // SurgeSynthesizer::channelController)
3804                         if (mc == 0 || mc == 6 || mc == 32 || mc == 38 || mc == 64 ||
3805                             (mc == 74 && synth->mpeEnabled) || (mc >= 98 && mc <= 101) ||
3806                             mc == 120 || mc == 123)
3807                             disabled = 1;
3808 
3809                         // we don't have any more CCs to cover, so break the loop
3810                         if (mc > 127)
3811                             break;
3812 
3813                         char name[128];
3814 
3815                         sprintf(name, "CC %d (%s) %s", mc, midicc_names[mc],
3816                                 (disabled == 1 ? "- RESERVED" : ""));
3817 
3818                         CCommandMenuItem *cmd = new CCommandMenuItem(CCommandMenuItem::Desc(name));
3819 
3820                         cmd->setActions([this, p, ptag, mc](CCommandMenuItem *men) {
3821                             if (ptag < n_global_params)
3822                                 p->midictrl = mc;
3823                             else
3824                             {
3825                                 int a = ptag;
3826                                 if (ptag >= (n_global_params + n_scene_params))
3827                                     a -= ptag;
3828 
3829                                 synth->storage.getPatch().param_ptr[a]->midictrl = mc;
3830                                 synth->storage.getPatch().param_ptr[a + n_scene_params]->midictrl =
3831                                     mc;
3832                             }
3833                         });
3834                         cmd->setEnabled(!disabled);
3835 
3836                         auto added = currentSub->addEntry(cmd);
3837 
3838                         if ((ptag < n_global_params && p->midictrl == mc) ||
3839                             (ptag > n_global_params &&
3840                              synth->storage.getPatch().param_ptr[ptag]->midictrl == mc))
3841                         {
3842                             added->setChecked();
3843                             added_to_menu->setChecked();
3844                         }
3845                     }
3846                 }
3847 
3848                 if (currentSub)
3849                 {
3850                     currentSub->forget();
3851                     currentSub = nullptr;
3852                 }
3853 
3854                 contextMenu->addEntry(midiSub,
3855                                       Surge::UI::toOSCaseForMenu("Assign Parameter To..."));
3856 
3857                 eid++;
3858 
3859                 if (synth->learn_param > -1 && synth->learn_param == p->id)
3860                     cancellearn = true;
3861 
3862                 std::string learnTag =
3863                     cancellearn ? "Abort Parameter MIDI Learn" : "MIDI Learn Parameter...";
3864                 addCallbackMenu(contextMenu, Surge::UI::toOSCaseForMenu(learnTag),
3865                                 [this, cancellearn, control, p] {
3866                                     if (cancellearn)
3867                                     {
3868                                         hideMidiLearnOverlay();
3869                                         synth->learn_param = -1;
3870                                     }
3871                                     else
3872                                     {
3873                                         showMidiLearnOverlay(control->getViewSize());
3874                                         synth->learn_param = p->id;
3875                                     }
3876                                 });
3877                 eid++;
3878 
3879                 if (p->midictrl >= 0)
3880                 {
3881                     char txt4[16];
3882                     decode_controllerid(txt4, p->midictrl);
3883                     sprintf(txt, "Clear Learned MIDI (%s ", txt4);
3884                     addCallbackMenu(
3885                         contextMenu,
3886                         Surge::UI::toOSCaseForMenu(txt) + midicc_names[p->midictrl] + ")",
3887                         [this, p, ptag]() {
3888                             if (ptag < n_global_params)
3889                             {
3890                                 p->midictrl = -1;
3891                             }
3892                             else
3893                             {
3894                                 int a = ptag;
3895                                 if (ptag >= (n_global_params + n_scene_params))
3896                                     a -= n_scene_params;
3897 
3898                                 synth->storage.getPatch().param_ptr[a]->midictrl = -1;
3899                                 synth->storage.getPatch().param_ptr[a + n_scene_params]->midictrl =
3900                                     -1;
3901                             }
3902                         });
3903                     eid++;
3904                 }
3905 
3906                 int n_ms = 0;
3907 
3908                 for (int ms = 1; ms < n_modsources; ms++)
3909                     if (synth->isActiveModulation(ptag, (modsources)ms))
3910                         n_ms++;
3911 
3912                 // see if we have any modulators that are unassigned, then create "Add Modulation
3913                 // from..." menu
3914                 if (n_ms != n_modsources)
3915                 {
3916                     COptionMenu *addModSub =
3917                         new COptionMenu(menuRect, 0, 0, 0, 0, VSTGUI::COptionMenu::kNoDrawStyle);
3918 
3919                     COptionMenu *addMacroSub =
3920                         new COptionMenu(menuRect, 0, 0, 0, 0, VSTGUI::COptionMenu::kNoDrawStyle);
3921                     COptionMenu *addVLFOSub =
3922                         new COptionMenu(menuRect, 0, 0, 0, 0, VSTGUI::COptionMenu::kNoDrawStyle);
3923                     COptionMenu *addSLFOSub =
3924                         new COptionMenu(menuRect, 0, 0, 0, 0, VSTGUI::COptionMenu::kNoDrawStyle);
3925                     COptionMenu *addEGSub =
3926                         new COptionMenu(menuRect, 0, 0, 0, 0, VSTGUI::COptionMenu::kNoDrawStyle);
3927                     COptionMenu *addMIDISub =
3928                         new COptionMenu(menuRect, 0, 0, 0, 0, VSTGUI::COptionMenu::kNoDrawStyle);
3929                     COptionMenu *addMiscSub =
3930                         new COptionMenu(menuRect, 0, 0, 0, 0, VSTGUI::COptionMenu::kNoDrawStyle);
3931 
3932                     for (int k = 1; k < n_modsources; k++)
3933                     {
3934                         modsources ms = (modsources)modsource_display_order[k];
3935 
3936                         if (!synth->isActiveModulation(ptag, ms) &&
3937                             synth->isValidModulation(ptag, ms))
3938                         {
3939                             char tmptxt[512];
3940                             sprintf(tmptxt, "%s", modulatorName(ms, false).c_str());
3941 
3942                             // TODO FIXME: Try not to be gross!
3943                             if (ms >= ms_ctrl1 && ms <= ms_ctrl1 + n_customcontrollers - 1)
3944                             {
3945                                 addCallbackMenu(addMacroSub, tmptxt, [this, p, control, ms]() {
3946                                     this->promptForUserValueEntry(p, control, ms);
3947                                 });
3948                             }
3949                             else if (ms >= ms_lfo1 && ms <= ms_lfo1 + n_lfos_voice - 1)
3950                             {
3951                                 addCallbackMenu(addVLFOSub, tmptxt, [this, p, control, ms]() {
3952                                     this->promptForUserValueEntry(p, control, ms);
3953                                 });
3954                             }
3955                             else if (ms >= ms_slfo1 && ms <= ms_slfo1 + n_lfos_scene - 1)
3956                             {
3957                                 addCallbackMenu(addSLFOSub, tmptxt, [this, p, control, ms]() {
3958                                     this->promptForUserValueEntry(p, control, ms);
3959                                 });
3960                             }
3961                             else if (ms >= ms_ampeg && ms <= ms_filtereg)
3962                             {
3963                                 addCallbackMenu(addEGSub, tmptxt, [this, p, control, ms]() {
3964                                     this->promptForUserValueEntry(p, control, ms);
3965                                 });
3966                             }
3967                             else if (ms >= ms_random_bipolar && ms <= ms_alternate_unipolar)
3968                             {
3969                                 addCallbackMenu(addMiscSub, tmptxt, [this, p, control, ms]() {
3970                                     this->promptForUserValueEntry(p, control, ms);
3971                                 });
3972                             }
3973                             else
3974                             {
3975                                 addCallbackMenu(addMIDISub, tmptxt, [this, p, control, ms]() {
3976                                     this->promptForUserValueEntry(p, control, ms);
3977                                 });
3978                             }
3979                         }
3980                     }
3981 
3982                     if (addMacroSub->getNbEntries())
3983                     {
3984                         addModSub->addEntry(addMacroSub, "Macros");
3985                     }
3986                     if (addVLFOSub->getNbEntries())
3987                     {
3988                         addModSub->addEntry(addVLFOSub, "Voice LFOs");
3989                     }
3990                     if (addSLFOSub->getNbEntries())
3991                     {
3992                         addModSub->addEntry(addSLFOSub, "Scene LFOs");
3993                     }
3994                     if (addEGSub->getNbEntries())
3995                     {
3996                         addModSub->addEntry(addEGSub, "Envelopes");
3997                     }
3998                     if (addMIDISub->getNbEntries())
3999                     {
4000                         addModSub->addEntry(addMIDISub, "MIDI");
4001                     }
4002                     if (addMiscSub->getNbEntries())
4003                     {
4004                         addModSub->addEntry(addMiscSub, "Internal");
4005                     }
4006 
4007                     if (addModSub->getNbEntries())
4008                     {
4009                         contextMenu->addSeparator(eid++);
4010                         contextMenu->addEntry(addModSub,
4011                                               Surge::UI::toOSCaseForMenu("Add Modulation from..."));
4012                     }
4013 
4014                     if (addMacroSub)
4015                     {
4016                         addMacroSub->forget();
4017                         addMacroSub = nullptr;
4018                     }
4019                     if (addVLFOSub)
4020                     {
4021                         addVLFOSub->forget();
4022                         addVLFOSub = nullptr;
4023                     }
4024                     if (addSLFOSub)
4025                     {
4026                         addSLFOSub->forget();
4027                         addSLFOSub = nullptr;
4028                     }
4029                     if (addEGSub)
4030                     {
4031                         addEGSub->forget();
4032                         addEGSub = nullptr;
4033                     }
4034                     if (addMIDISub)
4035                     {
4036                         addMIDISub->forget();
4037                         addMIDISub = nullptr;
4038                     }
4039                     if (addMiscSub)
4040                     {
4041                         addMiscSub->forget();
4042                         addMiscSub = nullptr;
4043                     }
4044 
4045                     if (addModSub)
4046                     {
4047                         addModSub->forget();
4048                         addModSub = nullptr;
4049                     }
4050 
4051                     eid++;
4052                 }
4053 
4054                 if (n_ms)
4055                 {
4056                     contextMenu->addSeparator(eid++);
4057 
4058                     for (int k = 1; k < n_modsources; k++)
4059                     {
4060                         modsources ms = (modsources)k;
4061                         if (synth->isActiveModulation(ptag, ms))
4062                         {
4063                             char modtxt[256];
4064                             p->get_display_of_modulation_depth(modtxt, synth->getModDepth(ptag, ms),
4065                                                                synth->isBipolarModulation(ms),
4066                                                                Parameter::Menu);
4067 
4068                             char tmptxt[512];
4069                             sprintf(tmptxt, "Edit %s -> %s: %s",
4070                                     (char *)modulatorName(ms, true).c_str(), p->get_full_name(),
4071                                     modtxt);
4072                             addCallbackMenu(contextMenu, tmptxt, [this, p, control, ms]() {
4073                                 this->promptForUserValueEntry(p, control, ms);
4074                             });
4075                             eid++;
4076                         }
4077                     }
4078                     contextMenu->addSeparator(eid++);
4079                     for (int k = 1; k < n_modsources; k++)
4080                     {
4081                         modsources ms = (modsources)k;
4082                         if (synth->isActiveModulation(ptag, ms))
4083                         {
4084                             char tmptxt[256];
4085                             snprintf(tmptxt, 256, "Clear %s -> %s",
4086                                      (char *)modulatorName(ms, true).c_str(), p->get_full_name());
4087                             // clear_ms[ms] = eid;
4088                             // contextMenu->addEntry(tmptxt, eid++);
4089                             addCallbackMenu(contextMenu, tmptxt, [this, ms, ptag]() {
4090                                 synth->clearModulation(ptag, (modsources)ms);
4091                                 refresh_mod();
4092                                 /*
4093                                 ** FIXME - this is a pretty big hammer to deal with
4094                                 ** #1477 - can we be more parsimonious?
4095                                 */
4096                                 synth->refresh_editor = true;
4097                             });
4098                             eid++;
4099                         }
4100                     }
4101                     if (n_ms > 1)
4102                     {
4103                         addCallbackMenu(contextMenu, Surge::UI::toOSCaseForMenu("Clear All"),
4104                                         [this, ptag]() {
4105                                             for (int ms = 1; ms < n_modsources; ms++)
4106                                             {
4107                                                 synth->clearModulation(ptag, (modsources)ms);
4108                                             }
4109                                             refresh_mod();
4110                                             synth->refresh_editor = true;
4111                                         });
4112                         eid++;
4113                     }
4114                 }
4115             } // end vt_float if statement
4116 
4117             if (p->ctrltype == ct_amplitude_clipper)
4118             {
4119                 std::string sc = std::string("Scene ") + (char)('A' + current_scene);
4120                 contextMenu->addSeparator(eid++);
4121                 // FIXME - add unified menu here
4122                 addCallbackMenu(contextMenu, Surge::UI::toOSCaseForMenu(sc + " Hard Clip Disabled"),
4123                                 [this]() {
4124                                     synth->storage.sceneHardclipMode[current_scene] =
4125                                         SurgeStorage::BYPASS_HARDCLIP;
4126                                 });
4127                 contextMenu->checkEntry(eid, synth->storage.sceneHardclipMode[current_scene] ==
4128                                                  SurgeStorage::BYPASS_HARDCLIP);
4129                 eid++;
4130 
4131                 addCallbackMenu(contextMenu,
4132                                 Surge::UI::toOSCaseForMenu(sc + " Hard Clip at 0 dBFS"), [this]() {
4133                                     synth->storage.sceneHardclipMode[current_scene] =
4134                                         SurgeStorage::HARDCLIP_TO_0DBFS;
4135                                 });
4136                 contextMenu->checkEntry(eid, synth->storage.sceneHardclipMode[current_scene] ==
4137                                                  SurgeStorage::HARDCLIP_TO_0DBFS);
4138                 eid++;
4139 
4140                 addCallbackMenu(contextMenu,
4141                                 Surge::UI::toOSCaseForMenu(sc + " Hard Clip at +18 dBFS"),
4142                                 [this]() {
4143                                     synth->storage.sceneHardclipMode[current_scene] =
4144                                         SurgeStorage::HARDCLIP_TO_18DBFS;
4145                                 });
4146                 contextMenu->checkEntry(eid, synth->storage.sceneHardclipMode[current_scene] ==
4147                                                  SurgeStorage::HARDCLIP_TO_18DBFS);
4148                 eid++;
4149             }
4150 
4151             if (p->ctrltype == ct_decibel_attenuation_clipper)
4152             {
4153                 contextMenu->addSeparator(eid++);
4154                 // FIXME - add unified menu here
4155 
4156                 addCallbackMenu(
4157                     contextMenu, Surge::UI::toOSCaseForMenu("Global Hard Clip Disabled"),
4158                     [this]() { synth->storage.hardclipMode = SurgeStorage::BYPASS_HARDCLIP; });
4159                 contextMenu->checkEntry(eid, synth->storage.hardclipMode ==
4160                                                  SurgeStorage::BYPASS_HARDCLIP);
4161                 eid++;
4162 
4163                 addCallbackMenu(
4164                     contextMenu, Surge::UI::toOSCaseForMenu("Global Hard Clip at 0 dBFS"),
4165                     [this]() { synth->storage.hardclipMode = SurgeStorage::HARDCLIP_TO_0DBFS; });
4166                 contextMenu->checkEntry(eid, synth->storage.hardclipMode ==
4167                                                  SurgeStorage::HARDCLIP_TO_0DBFS);
4168                 eid++;
4169 
4170                 addCallbackMenu(
4171                     contextMenu, Surge::UI::toOSCaseForMenu("Global Hard Clip at +18 dBFS"),
4172                     [this]() { synth->storage.hardclipMode = SurgeStorage::HARDCLIP_TO_18DBFS; });
4173                 contextMenu->checkEntry(eid, synth->storage.hardclipMode ==
4174                                                  SurgeStorage::HARDCLIP_TO_18DBFS);
4175                 eid++;
4176             }
4177 
4178 #if TARGET_VST3
4179             auto hostMenu = addVst3MenuForParams(contextMenu, synth->idForParameter(p), eid);
4180 #endif
4181 
4182             frame->addView(contextMenu); // add to frame
4183             contextMenu->popup();
4184             frame->removeView(contextMenu, true); // remove from frame and forget
4185 
4186 #if TARGET_VST3
4187             if (hostMenu)
4188                 hostMenu->release();
4189 #endif
4190 
4191             return 1;
4192         }
4193         // reset to default value
4194         else if (button & kDoubleClick)
4195         {
4196             if (synth->isValidModulation(ptag, modsource) && mod_editor)
4197             {
4198                 CModulationSourceButton *cms = (CModulationSourceButton *)gui_modsrc[modsource];
4199                 auto thisms = modsource;
4200                 if (cms && cms->hasAlternate && cms->useAlternate)
4201                     thisms = (modsources)cms->alternateId;
4202 
4203                 synth->clearModulation(ptag, thisms);
4204                 ((CSurgeSlider *)control)->setModValue(synth->getModulation(p->id, thisms));
4205                 ((CSurgeSlider *)control)->setModPresent(synth->isModDestUsed(p->id));
4206                 ((CSurgeSlider *)control)
4207                     ->setModCurrent(synth->isActiveModulation(p->id, thisms),
4208                                     synth->isBipolarModulation(thisms));
4209                 // control->setGhostValue(p->get_value_f01());
4210                 oscdisplay->invalid();
4211                 return 0;
4212             }
4213             else
4214             {
4215                 switch (p->ctrltype)
4216                 {
4217                 case ct_lfotype:
4218                     /*
4219                     ** This code resets you to default if you double click on control
4220                     ** but on the lfoshape UI this is undesirable; it means if you accidentally
4221                     ** control click on step sequencer, say, you go back to sin and lose your
4222                     ** edits. So supress
4223                     */
4224                     break;
4225                 case ct_freq_audible_with_tunability:
4226                 case ct_freq_audible_with_very_low_lowerbound:
4227                 {
4228                     if (p->extend_range || button & VSTGUI::CButton::kAlt)
4229                     {
4230                         auto kr = this->synth->storage.getPatch()
4231                                       .scene[current_scene]
4232                                       .keytrack_root.val.i;
4233                         p->set_value_f01(p->value_to_normalized(kr - 69));
4234                         control->setValue(p->get_value_f01());
4235                     }
4236                     else
4237                     {
4238                         p->set_value_f01(p->get_default_value_f01());
4239                         control->setValue(p->get_value_f01());
4240                     }
4241                     return 0;
4242                 }
4243                 default:
4244                 {
4245                     p->set_value_f01(p->get_default_value_f01());
4246                     control->setValue(p->get_value_f01());
4247                     if (oscdisplay && (p->ctrlgroup == cg_OSC))
4248                         oscdisplay->invalid();
4249                     if (lfodisplay && (p->ctrlgroup == cg_LFO))
4250                         lfodisplay->invalid();
4251                     control->invalid();
4252                     return 0;
4253                 }
4254                 }
4255             }
4256         }
4257         // exclusive mute/solo in the mixer
4258         else if (button & kControl)
4259         {
4260             if (p->ctrltype == ct_bool_mute)
4261             {
4262                 Parameter *o1 = &(synth->storage.getPatch().scene[current_scene].mute_o1);
4263                 Parameter *r23 = &(synth->storage.getPatch().scene[current_scene].mute_ring_23);
4264 
4265                 auto curr = o1;
4266 
4267                 while (curr <= r23)
4268                 {
4269                     if (curr->id == p->id)
4270                         curr->val.b = true;
4271                     else
4272                         curr->val.b = false;
4273 
4274                     curr++;
4275                 }
4276 
4277                 synth->refresh_editor = true;
4278             }
4279             else if (p->ctrltype == ct_bool_solo)
4280             {
4281                 Parameter *o1 = &(synth->storage.getPatch().scene[current_scene].solo_o1);
4282                 Parameter *r23 = &(synth->storage.getPatch().scene[current_scene].solo_ring_23);
4283 
4284                 auto curr = o1;
4285 
4286                 while (curr <= r23)
4287                 {
4288                     if (curr->id == p->id)
4289                         curr->val.b = true;
4290                     else
4291                         curr->val.b = false;
4292 
4293                     curr++;
4294                 }
4295 
4296                 synth->refresh_editor = true;
4297             }
4298             else
4299                 p->bound_value();
4300         }
4301     }
4302     return 0;
4303 }
4304 
effectSettingsBackgroundClick(int whichScene)4305 void SurgeGUIEditor::effectSettingsBackgroundClick(int whichScene)
4306 {
4307     CPoint where;
4308     CRect menuRect;
4309     frame->getCurrentMouseLocation(where);
4310     frame->localToFrame(where);
4311 
4312     menuRect.offset(where.x, where.y);
4313 
4314     auto effmen = new COptionMenu(menuRect, 0, 0, 0, 0,
4315                                   VSTGUI::COptionMenu::kNoDrawStyle |
4316                                       VSTGUI::COptionMenu::kMultipleCheckStyle);
4317 
4318     auto msurl = SurgeGUIEditor::helpURLForSpecial("fx-selector");
4319     auto hurl = SurgeGUIEditor::fullyResolvedHelpURL(msurl);
4320     std::string txt;
4321 
4322     addCallbackMenu(effmen, "[?] Effect Settings",
4323                     [hurl]() { Surge::UserInteractions::openURL(hurl); });
4324 
4325     effmen->addSeparator();
4326 
4327     std::string sc = std::string("Scene ") + (char)('A' + whichScene);
4328 
4329     txt = sc + Surge::UI::toOSCaseForMenu(" Hard Clip Disabled");
4330     auto hcmen = addCallbackMenu(effmen, txt.c_str(), [this, whichScene]() {
4331         this->synth->storage.sceneHardclipMode[whichScene] = SurgeStorage::BYPASS_HARDCLIP;
4332     });
4333     hcmen->setChecked(synth->storage.sceneHardclipMode[whichScene] ==
4334                       SurgeStorage::BYPASS_HARDCLIP);
4335 
4336     txt = sc + Surge::UI::toOSCaseForMenu(" Hard Clip at 0 dBFS");
4337     hcmen = addCallbackMenu(effmen, txt.c_str(), [this, whichScene]() {
4338         this->synth->storage.sceneHardclipMode[whichScene] = SurgeStorage::HARDCLIP_TO_0DBFS;
4339     });
4340     hcmen->setChecked(synth->storage.sceneHardclipMode[whichScene] ==
4341                       SurgeStorage::HARDCLIP_TO_0DBFS);
4342 
4343     txt = sc + Surge::UI::toOSCaseForMenu(" Hard Clip at +18 dBFS");
4344     hcmen = addCallbackMenu(effmen, txt.c_str(), [this, whichScene]() {
4345         this->synth->storage.sceneHardclipMode[whichScene] = SurgeStorage::HARDCLIP_TO_18DBFS;
4346     });
4347     hcmen->setChecked(synth->storage.sceneHardclipMode[whichScene] ==
4348                       SurgeStorage::HARDCLIP_TO_18DBFS);
4349 
4350     frame->addView(effmen);
4351     effmen->popup();
4352     frame->removeView(effmen, true);
4353 }
4354 
valueChanged(CControl * control)4355 void SurgeGUIEditor::valueChanged(CControl *control)
4356 {
4357     if (!frame)
4358         return;
4359     if (!editor_open)
4360         return;
4361     long tag = control->getTag();
4362 
4363     if (typeinDialog != nullptr && tag != tag_value_typein)
4364     {
4365         typeinDialog->setVisible(false);
4366         removeFromFrame.push_back(typeinDialog);
4367         typeinDialog = nullptr;
4368         typeinResetCounter = -1;
4369         typeinEditTarget = nullptr;
4370         typeinMode = Inactive;
4371     }
4372 
4373     if (tag == tag_status_zoom)
4374     {
4375         control->setValue(0);
4376         CPoint where;
4377         frame->getCurrentMouseLocation(where);
4378         frame->localToFrame(where);
4379 
4380         showZoomMenu(where);
4381         return;
4382     }
4383 
4384     if (tag == tag_lfo_menu)
4385     {
4386         control->setValue(0);
4387         CPoint where;
4388         frame->getCurrentMouseLocation(where);
4389         frame->localToFrame(where);
4390 
4391         showLfoMenu(where);
4392         return;
4393     }
4394 
4395     if (tag == tag_status_mpe)
4396     {
4397         toggleMPE();
4398         return;
4399     }
4400     if (tag == tag_status_tune)
4401     {
4402         toggleTuning();
4403         return;
4404     }
4405 
4406     if (tag == tag_mseg_edit)
4407     {
4408         if (control->getValue() > 0.5)
4409         {
4410             showMSEGEditor();
4411         }
4412         else
4413         {
4414             closeMSEGEditor();
4415         }
4416         return;
4417     }
4418 
4419     if ((tag >= tag_mod_source0) && (tag < tag_mod_source_end))
4420     {
4421         if (((CModulationSourceButton *)control)->event_is_drag)
4422         {
4423             int t = (tag - tag_mod_source0);
4424             ((ControllerModulationSource *)synth->storage.getPatch()
4425                  .scene[current_scene]
4426                  .modsources[t])
4427                 ->set_target01(control->getValue(), false);
4428 
4429             SurgeSynthesizer::ID tid;
4430             if (synth->fromSynthSideId(t + metaparam_offset - ms_ctrl1, tid))
4431             {
4432                 synth->sendParameterAutomation(tid, control->getValue());
4433             }
4434             return;
4435         }
4436         else
4437         {
4438             CModulationSourceButton *cms = (CModulationSourceButton *)control;
4439             int state = cms->get_state();
4440             modsources newsource = (modsources)(tag - tag_mod_source0);
4441             long buttons = 0; // context->getMouseButtons(); // temp fix vstgui 3.5
4442             bool ciep =
4443                 ((CModulationSourceButton *)control)->click_is_editpart && (newsource >= ms_lfo1);
4444             if (!ciep)
4445             {
4446                 switch (state & 3)
4447                 {
4448                 case 0:
4449                     modsource = newsource;
4450                     if (mod_editor)
4451                         mod_editor = true;
4452                     else
4453                         mod_editor = false;
4454                     queue_refresh = true;
4455                     refresh_mod();
4456                     break;
4457                 case 1:
4458                     modsource = newsource;
4459                     mod_editor = true;
4460                     refresh_mod();
4461                     break;
4462                 case 2:
4463                     modsource = newsource;
4464                     mod_editor = false;
4465                     refresh_mod();
4466                     break;
4467                 };
4468             }
4469 
4470             if (isLFO(newsource) && !(buttons & kShift))
4471             {
4472                 if (modsource_editor[current_scene] != newsource)
4473                 {
4474                     auto tabPosMem = Surge::Storage::getUserDefaultValue(
4475                         &(this->synth->storage), "rememberTabPositionsPerScene", 0);
4476 
4477                     if (tabPosMem)
4478                         modsource_editor[current_scene] = newsource;
4479                     else
4480                     {
4481                         for (int i = 0; i < n_scenes; i++)
4482                         {
4483                             modsource_editor[i] = newsource;
4484                         }
4485                     }
4486 
4487                     if (isAnyOverlayPresent(MSEG_EDITOR))
4488                     {
4489                         auto ld = &(synth->storage.getPatch().scene[current_scene].lfo[newsource -
4490                                                                                        ms_lfo1]);
4491                         if (ld->shape.val.i == lt_mseg)
4492                         {
4493                             showMSEGEditor();
4494                         }
4495                         else
4496                         {
4497                             closeMSEGEditor();
4498                         }
4499                     }
4500 
4501                     queue_refresh = true;
4502                 }
4503             }
4504         }
4505 
4506         return;
4507     }
4508 
4509     if ((tag == f1subtypetag) || (tag == f2subtypetag))
4510     {
4511         int idx = (tag == f2subtypetag) ? 1 : 0;
4512         auto csc = dynamic_cast<CSwitchControl *>(control);
4513         int valdir = csc->value_direction;
4514         csc->value_direction = 0;
4515 
4516         int a =
4517             synth->storage.getPatch().scene[current_scene].filterunit[idx].subtype.val.i + valdir;
4518         int t = synth->storage.getPatch().scene[current_scene].filterunit[idx].type.val.i;
4519         int nn =
4520             fut_subcount[synth->storage.getPatch().scene[current_scene].filterunit[idx].type.val.i];
4521         if (a >= nn)
4522             a = 0;
4523         if (a < 0)
4524             a = nn - 1;
4525         synth->storage.getPatch().scene[current_scene].filterunit[idx].subtype.val.i = a;
4526         synth->storage.subtypeMemory[current_scene][idx][t] = a;
4527         if (csc)
4528         {
4529             if (nn == 0)
4530                 csc->ivalue = 0;
4531             else
4532                 csc->ivalue = a + 1;
4533         }
4534         control->invalid();
4535         synth->switch_toggled_queued = true;
4536         return;
4537     }
4538 
4539     switch (tag)
4540     {
4541     case tag_scene_select:
4542     {
4543         current_scene = (int)(control->getValue() * 1.f) + 0.5f;
4544         synth->release_if_latched[synth->storage.getPatch().scene_active.val.i] = true;
4545         synth->storage.getPatch().scene_active.val.i = current_scene;
4546         // synth->storage.getPatch().param_ptr[scene_select_pid]->set_value_f01(control->getValue());
4547 
4548         if (isAnyOverlayPresent(MSEG_EDITOR))
4549         {
4550             auto ld = &(synth->storage.getPatch()
4551                             .scene[current_scene]
4552                             .lfo[modsource_editor[current_scene] - ms_lfo1]);
4553             if (ld->shape.val.i == lt_mseg)
4554             {
4555                 showMSEGEditor();
4556             }
4557             else
4558             {
4559                 closeMSEGEditor();
4560             }
4561         }
4562 
4563         queue_refresh = true;
4564         return;
4565     }
4566     break;
4567     case tag_patchname:
4568     {
4569         int id = ((CPatchBrowser *)control)->enqueue_sel_id;
4570         // synth->load_patch(id);
4571         enqueuePatchId = id;
4572 
4573 #if LINUX || TARGET_JUCE_UI
4574         // On linux the popup menu will be gone so we gotta process
4575         flushEnqueuedPatchId();
4576 #endif
4577         return;
4578     }
4579     break;
4580     case tag_mp_category:
4581     {
4582         if (isAnyOverlayPresent(STORE_PATCH))
4583         {
4584             closeStorePatchDialog();
4585         }
4586 
4587         if (control->getValue() > 0.5f)
4588             synth->incrementCategory(true);
4589         else
4590             synth->incrementCategory(false);
4591         return;
4592     }
4593     break;
4594     case tag_mp_patch:
4595     {
4596         if (isAnyOverlayPresent(STORE_PATCH))
4597         {
4598             closeStorePatchDialog();
4599         }
4600 
4601         auto insideCategory =
4602             Surge::Storage::getUserDefaultValue(&(this->synth->storage), "patchJogWraparound", 1);
4603 
4604         if (control->getValue() > 0.5f)
4605             synth->incrementPatch(true, insideCategory);
4606         else
4607             synth->incrementPatch(false, insideCategory);
4608         return;
4609     }
4610     break;
4611     case tag_mp_jogfx:
4612     {
4613         CFxMenu *fxm = dynamic_cast<CFxMenu *>(fxmenu);
4614         auto jog = [this, fxm](int byThis) {
4615             this->selectedFX[this->current_fx] =
4616                 std::max(this->selectedFX[this->current_fx] + byThis, -1);
4617             if (!fxm->loadSnapshotByIndex(this->selectedFX[this->current_fx]))
4618             {
4619                 // Try and go back to 0. This is the wrong behavior for negative jog
4620                 this->selectedFX[this->current_fx] = 0;
4621                 fxm->loadSnapshotByIndex(0);
4622             }
4623         };
4624 
4625         if (fxm)
4626         {
4627             if (fxm->selectedIdx >= 0 && fxm->selectedIdx != selectedFX[current_fx])
4628                 selectedFX[current_fx] = fxm->selectedIdx;
4629 
4630             if (control->getValue() > 0.5f)
4631             {
4632                 jog(+1);
4633             }
4634             else
4635             {
4636                 jog(-1);
4637             }
4638 
4639             if (fxPresetLabel)
4640             {
4641                 fxPresetLabel->setText(fxm->selectedName.c_str());
4642                 fxPresetName[this->current_fx] = fxm->selectedName;
4643             }
4644         }
4645         else
4646         {
4647         }
4648         return;
4649     }
4650     break;
4651     case tag_settingsmenu:
4652     {
4653         CRect r = control->getViewSize();
4654         CRect menuRect;
4655         CPoint where;
4656         frame->getCurrentMouseLocation(where);
4657         frame->localToFrame(where);
4658 
4659         menuRect.offset(where.x, where.y);
4660 
4661         useDevMenu = false;
4662         showSettingsMenu(menuRect);
4663     }
4664     break;
4665     case tag_osc_select:
4666     {
4667         auto tabPosMem = Surge::Storage::getUserDefaultValue(&(this->synth->storage),
4668                                                              "rememberTabPositionsPerScene", 0);
4669 
4670         if (tabPosMem)
4671             current_osc[current_scene] = (int)(control->getValue() * 2.f + 0.5f);
4672         else
4673         {
4674             for (int i = 0; i < n_scenes; i++)
4675             {
4676                 current_osc[i] = (int)(control->getValue() * 2.f + 0.5f);
4677             }
4678         }
4679 
4680         queue_refresh = true;
4681         return;
4682     }
4683     break;
4684     case tag_fx_select:
4685     {
4686         auto fxc = ((CEffectSettings *)control);
4687         int d = fxc->get_disable();
4688         synth->fx_suspend_bitmask = synth->storage.getPatch().fx_disable.val.i ^ d;
4689         synth->storage.getPatch().fx_disable.val.i = d;
4690         fxc->set_disable(d);
4691 
4692         int nfx = fxc->get_current();
4693         if (current_fx != nfx)
4694         {
4695             current_fx = nfx;
4696             queue_refresh = true;
4697         }
4698 
4699         return;
4700     }
4701     break;
4702     case tag_osc_menu:
4703     {
4704         synth->switch_toggled_queued = true;
4705         queue_refresh = true;
4706         synth->processThreadunsafeOperations();
4707         return;
4708     }
4709     break;
4710     case tag_fx_menu:
4711     {
4712         synth->load_fx_needed = true;
4713         // queue_refresh = true;
4714         synth->fx_reload[current_fx & 7] = true;
4715         synth->processThreadunsafeOperations();
4716 
4717         CFxMenu *fxm = dynamic_cast<CFxMenu *>(fxmenu);
4718         if (fxm && fxm->selectedIdx >= 0)
4719         {
4720             selectedFX[current_fx] = fxm->selectedIdx;
4721             fxPresetName[current_fx] = fxm->selectedName;
4722         }
4723         else if (fxPresetLabel)
4724         {
4725             fxPresetLabel->setText(fxm->selectedName.c_str());
4726             fxPresetName[this->current_fx] = fxm->selectedName;
4727         }
4728 
4729         return;
4730     }
4731     break;
4732     case tag_store:
4733     {
4734         patchdata p;
4735 
4736         p.name = synth->storage.getPatch().name;
4737         p.category = synth->storage.getPatch().category;
4738         p.author = synth->storage.getPatch().author;
4739         p.comments = synth->storage.getPatch().comment;
4740 
4741         string defaultAuthor =
4742             Surge::Storage::getUserDefaultValue(&(this->synth->storage), "defaultPatchAuthor", "");
4743         string defaultComment =
4744             Surge::Storage::getUserDefaultValue(&(this->synth->storage), "defaultPatchComment", "");
4745         string oldAuthor = "";
4746 
4747         if (!Surge::Storage::isValidUTF8(defaultAuthor))
4748         {
4749             defaultAuthor = "";
4750         }
4751         if (!Surge::Storage::isValidUTF8(defaultComment))
4752         {
4753             defaultComment = "";
4754         }
4755 
4756         if (p.author == "" && defaultAuthor != "")
4757         {
4758             p.author = defaultAuthor;
4759         }
4760 
4761         if (p.author != "" && defaultAuthor != "")
4762         {
4763             if (_stricmp(p.author.c_str(), defaultAuthor.c_str()))
4764             {
4765                 oldAuthor = p.author;
4766                 p.author = defaultAuthor;
4767             }
4768         }
4769 
4770         if (p.comments == "" && defaultComment != "")
4771         {
4772             p.comments = defaultComment;
4773         }
4774 
4775         if (oldAuthor != "")
4776         {
4777             if (p.comments == "")
4778                 p.comments += "Original patch by " + oldAuthor;
4779             else
4780                 p.comments += " (Original patch by " + oldAuthor + ")";
4781         }
4782 
4783         showStorePatchDialog();
4784 
4785         patchName->setText(p.name.c_str());
4786         patchCategory->setText(p.category.c_str());
4787         patchCreator->setText(p.author.c_str());
4788         patchComment->setText(p.comments.c_str());
4789     }
4790     break;
4791     case tag_store_ok:
4792     {
4793         // prevent duplicate execution of savePatch() by detecting if the Store Patch dialog is
4794         // displayed or not
4795         // FIXME: baconpaul will know a better and more correct way to fix this
4796         if (isAnyOverlayPresent(STORE_PATCH))
4797         {
4798             synth->storage.getPatch().name = patchName->getText();
4799             synth->storage.getPatch().author = patchCreator->getText();
4800             synth->storage.getPatch().category = patchCategory->getText();
4801             synth->storage.getPatch().comment = patchComment->getText();
4802 
4803             synth->storage.getPatch().patchTuning.tuningStoredInPatch =
4804                 patchTuning->getValue() > 0.5;
4805             if (synth->storage.getPatch().patchTuning.tuningStoredInPatch)
4806             {
4807                 if (synth->storage.isStandardScale)
4808                 {
4809                     synth->storage.getPatch().patchTuning.scaleContents = "";
4810                 }
4811                 else
4812                 {
4813                     synth->storage.getPatch().patchTuning.scaleContents =
4814                         synth->storage.currentScale.rawText;
4815                 }
4816                 if (synth->storage.isStandardMapping)
4817                 {
4818                     synth->storage.getPatch().patchTuning.mappingContents = "";
4819                 }
4820                 else
4821                 {
4822                     synth->storage.getPatch().patchTuning.mappingContents =
4823                         synth->storage.currentMapping.rawText;
4824                     synth->storage.getPatch().patchTuning.mappingName =
4825                         synth->storage.currentMapping.name;
4826                 }
4827             }
4828 
4829             // Ignore whatever comes from the DAW
4830             synth->storage.getPatch().dawExtraState.isPopulated = false;
4831 
4832             synth->savePatch();
4833 
4834             closeStorePatchDialog();
4835             frame->setDirty();
4836         }
4837     }
4838     break;
4839     case tag_store_cancel:
4840     {
4841         closeStorePatchDialog();
4842         frame->setDirty();
4843     }
4844     break;
4845     case tag_editor_overlay_close:
4846     {
4847         // So can I find an editor overlay parent
4848         VSTGUI::CView *p = control;
4849         auto tagToNuke = NO_EDITOR;
4850         while (p)
4851         {
4852             p = p->getParentView();
4853             for (auto el : editorOverlay)
4854             {
4855                 if (el.second == p)
4856                 {
4857                     tagToNuke = el.first;
4858                     p = nullptr;
4859                     break;
4860                 }
4861             }
4862         }
4863         if (tagToNuke != NO_EDITOR)
4864         {
4865             dismissEditorOfType(tagToNuke);
4866         }
4867     }
4868     break;
4869     case tag_miniedit_ok:
4870     case tag_miniedit_cancel:
4871     {
4872         if (minieditOverlay != nullptr)
4873         {
4874             if (minieditTypein)
4875             {
4876                 auto q = minieditTypein->getText().getString();
4877                 if (tag == tag_miniedit_ok)
4878                 {
4879                     minieditOverlayDone(q.c_str());
4880                 }
4881                 minieditTypein->looseFocus();
4882             }
4883             minieditOverlay->setVisible(false);
4884             removeFromFrame.push_back(minieditOverlay);
4885             minieditOverlay = nullptr;
4886         }
4887     }
4888     break;
4889     case tag_value_typein:
4890     {
4891         if (typeinDialog && typeinMode != Inactive)
4892         {
4893             std::string t = typeinValue->getText().getString();
4894             bool isInvalid = false;
4895             if (typeinMode == Param && typeinEditTarget && typeinModSource > 0)
4896             {
4897                 bool valid = false;
4898                 auto mv = typeinEditTarget->calculate_modulation_value_from_string(t, valid);
4899 
4900                 if (!valid)
4901                 {
4902                     isInvalid = true;
4903                 }
4904                 else
4905                 {
4906                     synth->setModulation(typeinEditTarget->id, (modsources)typeinModSource, mv);
4907                     synth->refresh_editor = true;
4908 
4909                     typeinDialog->setVisible(false);
4910                     removeFromFrame.push_back(typeinDialog);
4911                     typeinDialog = nullptr;
4912                     typeinResetCounter = -1;
4913                     typeinEditTarget = nullptr;
4914                     typeinMode = Inactive;
4915                 }
4916             }
4917             else if (typeinMode == Param && typeinEditTarget &&
4918                      typeinEditTarget->set_value_from_string(t))
4919             {
4920                 repushAutomationFor(typeinEditTarget);
4921                 isInvalid = false;
4922                 synth->refresh_editor = true;
4923                 typeinDialog->setVisible(false);
4924                 removeFromFrame.push_back(typeinDialog);
4925                 typeinDialog = nullptr;
4926                 typeinResetCounter = -1;
4927                 typeinEditTarget = nullptr;
4928                 typeinMode = Inactive;
4929             }
4930             else if (typeinMode == Control)
4931             {
4932                 auto cms = ((ControllerModulationSource *)synth->storage.getPatch()
4933                                 .scene[current_scene]
4934                                 .modsources[typeinModSource]);
4935                 bool bp = cms->is_bipolar();
4936                 float val = std::atof(t.c_str()) / 100.0;
4937                 if ((bp && val >= -1 && val <= 1) || (val >= 0 && val <= 1))
4938                 {
4939                     cms->output = val;
4940                     cms->target = val;
4941                     // This doesn't seem to work so hammer away
4942                     if (typeinEditControl)
4943                     {
4944                         typeinEditControl->invalid();
4945                     }
4946                     synth->refresh_editor = true;
4947                 }
4948                 else
4949                 {
4950                     isInvalid = true;
4951                 }
4952             }
4953             else
4954             {
4955                 isInvalid = true;
4956             }
4957 
4958             if (isInvalid)
4959             {
4960                 auto l = typeinLabel->getText().getString();
4961                 promptForUserValueEntry(typeinEditTarget, typeinEditControl, typeinModSource);
4962                 typeinResetCounter = 20;
4963                 typeinResetLabel = l;
4964                 typeinLabel->setText("Invalid Entry");
4965                 typeinValue->setText(t.c_str());
4966                 typeinLabel->setFontColor(currentSkin->getColor(Colors::Dialog::Label::Error));
4967             }
4968         }
4969     }
4970     break;
4971     default:
4972     {
4973         int ptag = tag - start_paramtags;
4974 
4975         if ((ptag >= 0) && (ptag < synth->storage.getPatch().param_ptr.size()))
4976         {
4977             Parameter *p = synth->storage.getPatch().param_ptr[ptag];
4978             if (p->is_nonlocal_on_change())
4979                 frame->invalid();
4980 
4981             char pname[256], pdisp[128], txt[128];
4982             bool modulate = false;
4983 
4984             // This allows us to turn on and off the editor. FIXME mseg check it
4985             if (p->ctrltype == ct_lfotype)
4986                 synth->refresh_editor = true;
4987 
4988             if ((p->ctrlstyle & kNoPopup))
4989             {
4990                 auto iw = dynamic_cast<CParameterTooltip *>(infowindow);
4991                 if (iw && iw->isVisible())
4992                     iw->Hide();
4993             }
4994 
4995             if (modsource && mod_editor && synth->isValidModulation(p->id, modsource) &&
4996                 dynamic_cast<CSurgeSlider *>(control) != nullptr)
4997             {
4998                 modsources thisms = modsource;
4999                 if (gui_modsrc[modsource])
5000                 {
5001                     CModulationSourceButton *cms = (CModulationSourceButton *)gui_modsrc[modsource];
5002                     if (cms->hasAlternate && cms->useAlternate)
5003                         thisms = (modsources)cms->alternateId;
5004                 }
5005                 bool quantize_mod = frame->getCurrentMouseButtons() & kControl;
5006                 float mv = ((CSurgeSlider *)control)->getModValue();
5007                 if (quantize_mod)
5008                 {
5009                     mv = p->quantize_modulation(mv);
5010                     // maybe setModValue here
5011                 }
5012 
5013                 synth->setModulation(ptag, thisms, mv);
5014                 ((CSurgeSlider *)control)->setModPresent(synth->isModDestUsed(p->id));
5015                 ((CSurgeSlider *)control)
5016                     ->setModCurrent(synth->isActiveModulation(p->id, thisms),
5017                                     synth->isBipolarModulation(thisms));
5018 
5019                 SurgeSynthesizer::ID ptagid;
5020                 if (synth->fromSynthSideId(ptag, ptagid))
5021                     synth->getParameterName(ptagid, txt);
5022                 sprintf(pname, "%s -> %s", modulatorName(thisms, true).c_str(), txt);
5023                 ModulationDisplayInfoWindowStrings mss;
5024                 p->get_display_of_modulation_depth(pdisp, synth->getModDepth(ptag, thisms),
5025                                                    synth->isBipolarModulation(thisms),
5026                                                    Parameter::InfoWindow, &mss);
5027                 if (mss.val != "")
5028                 {
5029                     ((CParameterTooltip *)infowindow)->setLabel(pname, pdisp);
5030                     ((CParameterTooltip *)infowindow)->setMDIWS(mss);
5031                 }
5032                 else
5033                 {
5034                     ((CParameterTooltip *)infowindow)->setLabel(pname, pdisp);
5035                     ((CParameterTooltip *)infowindow)->clearMDIWS();
5036                 }
5037                 modulate = true;
5038 
5039                 if (isCustomController(modsource))
5040                 {
5041                     int ccid = modsource - ms_ctrl1;
5042                     char *lbl = synth->storage.getPatch().CustomControllerLabel[ccid];
5043 
5044                     if ((lbl[0] == '-') && !lbl[1])
5045                     {
5046                         strxcpy(lbl, p->get_name(), 15);
5047                         synth->storage.getPatch().CustomControllerLabel[ccid][15] = 0;
5048                         ((CModulationSourceButton *)gui_modsrc[modsource])->setlabel(lbl);
5049                         ((CModulationSourceButton *)gui_modsrc[modsource])->invalid();
5050                     }
5051                 }
5052             }
5053             else
5054             {
5055                 auto val = control->getValue();
5056                 if (p->ctrltype == ct_scenemode)
5057                 {
5058 
5059                     /*
5060                     ** See the comment in the constructor of ct_scenemode above
5061                     */
5062                     auto cs2 = dynamic_cast<CHSwitch2 *>(control);
5063                     auto im = 0;
5064                     if (cs2)
5065                     {
5066                         im = cs2->getIValue();
5067                         if (im == 3)
5068                             im = 2;
5069                         else if (im == 2)
5070                             im = 3;
5071                         val = Parameter::intScaledToFloat(im, n_scene_modes - 1);
5072                     }
5073 
5074                     /*
5075                     ** Now I also need to toggle the split key state
5076                     */
5077                     auto nf = dynamic_cast<CNumberField *>(splitpointControl);
5078                     if (nf)
5079                     {
5080                         int cm = nf->getControlMode();
5081                         if (im == sm_chsplit && cm != cm_midichannel_from_127)
5082                         {
5083                             nf->setControlMode(cm_midichannel_from_127);
5084                             nf->invalid();
5085                         }
5086                         else if (im == sm_split && cm != cm_notename)
5087                         {
5088                             nf->setControlMode(cm_notename);
5089                             nf->invalid();
5090                         }
5091                         else if ((im == sm_single || im == sm_dual) && cm != cm_none)
5092                         {
5093                             nf->setControlMode(cm_none);
5094                             nf->invalid();
5095                         }
5096                     }
5097                 }
5098 
5099                 bool force_integer = frame->getCurrentMouseButtons() & kControl;
5100                 SurgeSynthesizer::ID ptagid;
5101                 synth->fromSynthSideId(ptag, ptagid);
5102                 if (synth->setParameter01(ptagid, val, false, force_integer))
5103                 {
5104                     queue_refresh = true;
5105                     return;
5106                 }
5107                 else
5108                 {
5109                     synth->sendParameterAutomation(ptagid, synth->getParameter01(ptagid));
5110 
5111                     if (dynamic_cast<CSurgeSlider *>(control) != nullptr)
5112                         ((CSurgeSlider *)control)->SetQuantitizedDispValue(p->get_value_f01());
5113                     else
5114                         control->invalid();
5115                     synth->getParameterName(ptagid, pname);
5116                     synth->getParameterDisplay(ptagid, pdisp);
5117                     char pdispalt[256];
5118                     synth->getParameterDisplayAlt(ptagid, pdispalt);
5119                     ((CParameterTooltip *)infowindow)->setLabel(0, pdisp, pdispalt);
5120                     ((CParameterTooltip *)infowindow)->clearMDIWS();
5121                     if (p->ctrltype == ct_polymode)
5122                         modulate = true;
5123                 }
5124 
5125                 if (p->ctrltype == ct_bool_unipolar || p->ctrltype == ct_lfotype)
5126                 {
5127                     // The green line might change so...
5128                     refresh_mod();
5129                 }
5130             }
5131             if (!queue_refresh)
5132             {
5133                 if (!(p->ctrlstyle & kNoPopup))
5134                 {
5135                     draw_infowindow(ptag, control, modulate);
5136                 }
5137 
5138                 if (oscdisplay && ((p->ctrlgroup == cg_OSC) || (p->ctrltype == ct_character)))
5139                 {
5140                     oscdisplay->setDirty();
5141                     oscdisplay->invalid();
5142                 }
5143                 if (lfodisplay && (p->ctrlgroup == cg_LFO))
5144                 {
5145                     lfodisplay->setDirty();
5146                     lfodisplay->invalid();
5147                 }
5148                 for (auto el : editorOverlay)
5149                 {
5150                     el.second->invalid();
5151                 }
5152             }
5153             if (p->ctrltype == ct_filtertype)
5154             {
5155                 auto *subsw = dynamic_cast<CSwitchControl *>(filtersubtype[p->ctrlgroup_entry]);
5156                 if (subsw)
5157                 {
5158                     int sc = fut_subcount[p->val.i];
5159 
5160                     subsw->imax = sc;
5161                 }
5162             }
5163         }
5164 
5165         break;
5166     }
5167     }
5168 
5169     if ((tag == (f1subtypetag - 1)) || (tag == (f2subtypetag - 1)))
5170     {
5171         int idx = (tag == (f2subtypetag - 1)) ? 1 : 0;
5172 
5173         int a = synth->storage.getPatch().scene[current_scene].filterunit[idx].subtype.val.i;
5174         int nn =
5175             fut_subcount[synth->storage.getPatch().scene[current_scene].filterunit[idx].type.val.i];
5176         if (a >= nn)
5177             a = 0;
5178         synth->storage.getPatch().scene[current_scene].filterunit[idx].subtype.val.i = a;
5179         if (!nn)
5180             ((CSwitchControl *)filtersubtype[idx])->ivalue = 0;
5181         else
5182             ((CSwitchControl *)filtersubtype[idx])->ivalue = a + 1;
5183 
5184         filtersubtype[idx]->setDirty();
5185         filtersubtype[idx]->invalid();
5186     }
5187 
5188     if (tag == fmconfig_tag)
5189     {
5190         // FM depth control
5191         int i = synth->storage.getPatch().scene[current_scene].fm_depth.id;
5192         if (param[i] && dynamic_cast<CSurgeSlider *>(param[i]) != nullptr)
5193             ((CSurgeSlider *)param[i])->disabled =
5194                 (synth->storage.getPatch().scene[current_scene].fm_switch.val.i == fm_off);
5195 
5196         param[i]->setDirty();
5197         param[i]->invalid();
5198     }
5199 
5200     if (tag == filterblock_tag)
5201     {
5202         // pan2 control
5203         int i = synth->storage.getPatch().scene[current_scene].width.id;
5204         if (param[i] && dynamic_cast<CSurgeSlider *>(param[i]) != nullptr)
5205             ((CSurgeSlider *)param[i])->disabled =
5206                 (synth->storage.getPatch().scene[current_scene].filterblock_configuration.val.i !=
5207                  fc_stereo) &&
5208                 (synth->storage.getPatch().scene[current_scene].filterblock_configuration.val.i !=
5209                  fc_wide);
5210 
5211         param[i]->setDirty();
5212         param[i]->invalid();
5213 
5214         // feedback control
5215         i = synth->storage.getPatch().scene[current_scene].feedback.id;
5216         if (param[i] && dynamic_cast<CSurgeSlider *>(param[i]) != nullptr)
5217             ((CSurgeSlider *)param[i])->disabled =
5218                 (synth->storage.getPatch().scene[current_scene].filterblock_configuration.val.i ==
5219                  fc_serial1);
5220 
5221         param[i]->setDirty();
5222         param[i]->invalid();
5223     }
5224 
5225     if (tag == fxbypass_tag) // still do the normal operation, that's why it's outside the
5226                              // switch-statement
5227     {
5228         if (ccfxconf)
5229         {
5230             ((CEffectSettings *)ccfxconf)->set_bypass(synth->storage.getPatch().fx_bypass.val.i);
5231             ccfxconf->invalid();
5232         }
5233 
5234         switch (synth->storage.getPatch().fx_bypass.val.i)
5235         {
5236         case fxb_no_fx:
5237             synth->fx_suspend_bitmask = synth->fx_suspend_bitmask | 0xff;
5238             break;
5239         case fxb_scene_fx_only:
5240             synth->fx_suspend_bitmask = synth->fx_suspend_bitmask | 0xf0;
5241             break;
5242         case fxb_no_sends:
5243             synth->fx_suspend_bitmask = synth->fx_suspend_bitmask | 0x30;
5244             break;
5245         case fxb_all_fx:
5246         default:
5247             break;
5248         }
5249     }
5250 }
5251 
5252 //------------------------------------------------------------------------------------------------
5253 
beginEdit(long tag)5254 void SurgeGUIEditor::beginEdit(long tag)
5255 {
5256     if (tag < start_paramtags)
5257     {
5258         return;
5259     }
5260 
5261     int synthidx = tag - start_paramtags;
5262     SurgeSynthesizer::ID did;
5263 
5264     if (synth->fromSynthSideId(synthidx, did))
5265     {
5266         super::beginEdit(did.getDawSideId());
5267     }
5268 }
5269 
5270 //------------------------------------------------------------------------------------------------
5271 
endEdit(long tag)5272 void SurgeGUIEditor::endEdit(long tag)
5273 {
5274     if (tag < start_paramtags)
5275     {
5276         return;
5277     }
5278 
5279     int synthidx = tag - start_paramtags;
5280     SurgeSynthesizer::ID did;
5281 
5282     if (synth->fromSynthSideId(synthidx, did))
5283     {
5284         super::endEdit(did.getDawSideId());
5285     }
5286 }
5287 
5288 //------------------------------------------------------------------------------------------------
5289 
controlBeginEdit(VSTGUI::CControl * control)5290 void SurgeGUIEditor::controlBeginEdit(VSTGUI::CControl *control)
5291 {
5292 #if TARGET_AUDIOUNIT
5293     long tag = control->getTag();
5294     int ptag = tag - start_paramtags;
5295 
5296     if (tag >= tag_mod_source0 + ms_ctrl1 && tag <= tag_mod_source0 + ms_ctrl8)
5297     {
5298         ptag = metaparam_offset + tag - tag_mod_source0 - ms_ctrl1;
5299         SurgeSynthesizer::ID did;
5300         if (synth->fromSynthSideId(ptag, did))
5301         {
5302             synth->getParent()->ParameterBeginEdit(did.getDawSideIndex());
5303         }
5304     }
5305     else if ((ptag >= 0) && (ptag < synth->storage.getPatch().param_ptr.size()))
5306     {
5307         SurgeSynthesizer::ID did;
5308 
5309         if (synth->fromSynthSideId(ptag, did))
5310         {
5311             synth->getParent()->ParameterBeginEdit(did.getDawSideIndex());
5312         }
5313     }
5314 
5315 #endif
5316 
5317 #if TARGET_VST3
5318     /*
5319      * I am sure this is us doing something wrong, but the first time through
5320      * of an edit of a discrete value (the digital/analog for instance) the
5321      * beignEdit starts sending me the old value over and over which isn't what
5322      * I want, and that old value is gotten when beginEdit is called, not when
5323      * endEdit is called. So this code, in LIve, establishes the value as correct
5324      * at the beginning of beginEdit. Which is odd but fixes issue #3283.
5325      */
5326     if (synth->hostProgram.find("Ableton") != string::npos)
5327     {
5328         long tag = control->getTag();
5329         int ptag = tag - start_paramtags;
5330         if (ptag >= 0 && ptag < synth->storage.getPatch().param_ptr.size())
5331         {
5332             auto id = synth->idForParameter(synth->storage.getPatch().param_ptr[ptag]);
5333             synth->setParameter01(id, control->getValue(), false);
5334         }
5335     }
5336 #endif
5337 
5338 #if TARGET_JUCE_UI
5339     long tag = control->getTag();
5340     int ptag = tag - start_paramtags;
5341     if (ptag >= 0 && ptag < synth->storage.getPatch().param_ptr.size())
5342     {
5343         _effect->beginParameterEdit(synth->storage.getPatch().param_ptr[ptag]);
5344     }
5345 #endif
5346 }
5347 
5348 //------------------------------------------------------------------------------------------------
5349 
controlEndEdit(VSTGUI::CControl * control)5350 void SurgeGUIEditor::controlEndEdit(VSTGUI::CControl *control)
5351 {
5352 #if TARGET_AUDIOUNIT
5353     long tag = control->getTag();
5354     int ptag = tag - start_paramtags;
5355     if (tag >= tag_mod_source0 + ms_ctrl1 && tag <= tag_mod_source0 + ms_ctrl8)
5356     {
5357         ptag = metaparam_offset + tag - tag_mod_source0 - ms_ctrl1;
5358         SurgeSynthesizer::ID did;
5359         if (synth->fromSynthSideId(ptag, did))
5360             synth->getParent()->ParameterEndEdit(did.getDawSideIndex());
5361     }
5362     else if ((ptag >= 0) && (ptag < synth->storage.getPatch().param_ptr.size()))
5363     {
5364         SurgeSynthesizer::ID did;
5365 
5366         if (synth->fromSynthSideId(ptag, did))
5367         {
5368             synth->getParent()->ParameterEndEdit(did.getDawSideIndex());
5369         }
5370     }
5371 #endif
5372 
5373 #if TARGET_JUCE_UI
5374     long tag = control->getTag();
5375     int ptag = tag - start_paramtags;
5376     if (ptag >= 0 && ptag < synth->storage.getPatch().param_ptr.size())
5377     {
5378         _effect->endParameterEdit(synth->storage.getPatch().param_ptr[ptag]);
5379     }
5380 #endif
5381 
5382     if (((CParameterTooltip *)infowindow)->isVisible())
5383     {
5384         auto cs = dynamic_cast<CSurgeSlider *>(control);
5385         if (cs == nullptr || cs->hasBeenDraggedDuringMouseGesture)
5386             ((CParameterTooltip *)infowindow)->Hide();
5387         else
5388         {
5389             clear_infoview_countdown = 15;
5390         }
5391     }
5392 }
5393 
5394 //------------------------------------------------------------------------------------------------
5395 
draw_infowindow(int ptag,CControl * control,bool modulate,bool forceMB)5396 void SurgeGUIEditor::draw_infowindow(int ptag, CControl *control, bool modulate, bool forceMB)
5397 {
5398     long buttons = 1; // context?context->getMouseButtons():1;
5399 
5400     if (buttons && forceMB)
5401         return; // don't draw via CC if MB is down
5402 
5403     // A heuristic
5404     auto ml = ((CParameterTooltip *)infowindow)->getMaxLabelLen();
5405     auto iff = 148;
5406     // This is just empirical. It would be lovely to use the actual string width but that needs a
5407     // draw context of for these to be TextLabels so we can call sizeToFit
5408     if (ml > 24)
5409         iff += (ml - 24) * 5;
5410 
5411     auto modValues =
5412         Surge::Storage::getUserDefaultValue(&(this->synth->storage), "modWindowShowsValues", 0);
5413 
5414     ((CParameterTooltip *)infowindow)->setExtendedMDIWS(modValues);
5415     CRect r(0, 0, iff, 18);
5416     if (modulate)
5417     {
5418         int hasMDIWS = ((CParameterTooltip *)infowindow)->hasMDIWS();
5419         r.bottom += (hasMDIWS & modValues ? 36 : 18);
5420         if (modValues)
5421             r.right += 20;
5422     }
5423 
5424     CRect r2 = control->getViewSize();
5425 
5426     // OK this is a heuristic to stop deform overpainting and stuff
5427     if (r2.bottom > getWindowSizeY() - r.getHeight() - 2)
5428     {
5429         // stick myself on top please
5430         r.offset((r2.left / 150) * 150, r2.top - r.getHeight() - 2);
5431     }
5432     else
5433     {
5434         r.offset((r2.left / 150) * 150, r2.bottom);
5435     }
5436 
5437     if (r.bottom > getWindowSizeY() - 2)
5438     {
5439         int ao = (getWindowSizeY() - 2 - r.bottom);
5440         r.offset(0, ao);
5441     }
5442 
5443     if (r.right > getWindowSizeX() - 2)
5444     {
5445         int ao = (getWindowSizeX() - 2 - r.right);
5446         r.offset(ao, 0);
5447     }
5448 
5449     if (r.left < 0)
5450     {
5451         int ao = 2 - r.left;
5452         r.offset(ao, 0);
5453     }
5454 
5455     if (r.top < 0)
5456     {
5457         int ao = 2 - r.top;
5458         r.offset(0, ao);
5459     }
5460 
5461     if (buttons || forceMB)
5462     {
5463         // make sure an infowindow doesn't appear twice
5464         if (((CParameterTooltip *)infowindow)->isVisible())
5465         {
5466             ((CParameterTooltip *)infowindow)->Hide();
5467             ((CParameterTooltip *)infowindow)->invalid();
5468         }
5469 
5470         ((CParameterTooltip *)infowindow)->setViewSize(r);
5471         ((CParameterTooltip *)infowindow)->Show();
5472         infowindow->invalid();
5473         // on Linux the infoview closes too soon
5474 #if LINUX
5475         clear_infoview_countdown = 100;
5476 #else
5477         clear_infoview_countdown = 40;
5478 #endif
5479     }
5480     else
5481     {
5482         ((CParameterTooltip *)infowindow)->Hide();
5483         frame->invalidRect(r);
5484     }
5485 }
5486 
applyParameterOffset(long id)5487 long SurgeGUIEditor::applyParameterOffset(long id) { return id - start_paramtags; }
5488 
unapplyParameterOffset(long id)5489 long SurgeGUIEditor::unapplyParameterOffset(long id) { return id + start_paramtags; }
5490 
5491 // Status Panel Callbacks
toggleMPE()5492 void SurgeGUIEditor::toggleMPE()
5493 {
5494     this->synth->mpeEnabled = !this->synth->mpeEnabled;
5495     if (statusMPE)
5496     {
5497         statusMPE->setValue(this->synth->mpeEnabled ? 1 : 0);
5498         statusMPE->invalid();
5499     }
5500 }
showZoomMenu(VSTGUI::CPoint & where)5501 void SurgeGUIEditor::showZoomMenu(VSTGUI::CPoint &where)
5502 {
5503     CRect menuRect;
5504     menuRect.offset(where.x, where.y);
5505     auto m = makeZoomMenu(menuRect, true);
5506 
5507     frame->addView(m);
5508     m->setDirty();
5509     m->popup();
5510     frame->removeView(m, true);
5511 }
5512 
showMPEMenu(VSTGUI::CPoint & where)5513 void SurgeGUIEditor::showMPEMenu(VSTGUI::CPoint &where)
5514 {
5515     CRect menuRect;
5516     menuRect.offset(where.x, where.y);
5517     auto m = makeMpeMenu(menuRect, true);
5518 
5519     frame->addView(m);
5520     m->setDirty();
5521     m->popup();
5522     frame->removeView(m, true);
5523 }
showLfoMenu(VSTGUI::CPoint & where)5524 void SurgeGUIEditor::showLfoMenu(VSTGUI::CPoint &where)
5525 {
5526     CRect menuRect;
5527     menuRect.offset(where.x, where.y);
5528     auto m = makeLfoMenu(menuRect);
5529 
5530     frame->addView(m);
5531     m->setDirty();
5532     m->popup();
5533     frame->removeView(m, true);
5534 }
5535 
toggleTuning()5536 void SurgeGUIEditor::toggleTuning()
5537 {
5538     synth->storage.toggleTuningToCache();
5539 
5540     if (statusTune)
5541     {
5542         bool hasmts = synth->storage.oddsound_mts_client && synth->storage.oddsound_mts_active;
5543         statusTune->setValue(this->synth->storage.isStandardTuning ? hasmts : 1);
5544     }
5545 
5546     this->synth->refresh_editor = true;
5547 }
showTuningMenu(VSTGUI::CPoint & where)5548 void SurgeGUIEditor::showTuningMenu(VSTGUI::CPoint &where)
5549 {
5550     CRect menuRect;
5551     menuRect.offset(where.x, where.y);
5552     auto m = makeTuningMenu(menuRect, true);
5553 
5554     frame->addView(m);
5555     m->setDirty();
5556     m->popup();
5557     frame->removeView(m, true);
5558 }
5559 
scaleFileDropped(std::string fn)5560 void SurgeGUIEditor::scaleFileDropped(std::string fn)
5561 {
5562     try
5563     {
5564         this->synth->storage.retuneToScale(Tunings::readSCLFile(fn));
5565         this->synth->refresh_editor = true;
5566     }
5567     catch (Tunings::TuningError &e)
5568     {
5569         Surge::UserInteractions::promptError(e.what(), "SCL Error");
5570     }
5571 }
5572 
mappingFileDropped(std::string fn)5573 void SurgeGUIEditor::mappingFileDropped(std::string fn)
5574 {
5575     try
5576     {
5577         this->synth->storage.remapToKeyboard(Tunings::readKBMFile(fn));
5578         this->synth->refresh_editor = true;
5579     }
5580     catch (Tunings::TuningError &e)
5581     {
5582         Surge::UserInteractions::promptError(e.what(), "KBM Error");
5583     }
5584 }
5585 
doesZoomFitToScreen(float zf,float & correctedZf)5586 bool SurgeGUIEditor::doesZoomFitToScreen(float zf, float &correctedZf)
5587 {
5588 #if !LINUX
5589     correctedZf = zf;
5590     return true;
5591 #endif
5592 
5593     CRect screenDim = Surge::GUI::getScreenDimensions(getFrame());
5594 
5595     float baseW = getWindowSizeX();
5596     float baseH = getWindowSizeY();
5597 
5598     /*
5599     ** Window decoration takes up some of the screen so don't zoom to full screen dimensions.
5600     ** This heuristic seems to work on Windows 10 and macOS 10.14 well enough.
5601     ** Keep these as integers to be consistent with the other zoom factors, and to make
5602     ** the error message cleaner.
5603     */
5604     int maxScreenUsage = 90;
5605 
5606     /*
5607     ** In the startup path we may not have a clean window yet to give us a trustworthy
5608     ** screen dimension; so allow callers to suppress this check with an optional
5609     ** variable and set it only in the constructor of SurgeGUIEditor
5610     */
5611     if (zf != 100.0 && zf > 100 && screenDim.getHeight() > 0 && screenDim.getWidth() > 0 &&
5612         ((baseW * zf / 100.0) > maxScreenUsage * screenDim.getWidth() / 100.0 ||
5613          (baseH * zf / 100.0) > maxScreenUsage * screenDim.getHeight() / 100.0))
5614     {
5615         correctedZf = findLargestFittingZoomBetween(100.0, zf, 5, maxScreenUsage, baseW, baseH);
5616         return false;
5617     }
5618     else
5619     {
5620         correctedZf = zf;
5621         return true;
5622     }
5623 }
5624 
resizeWindow(float zf)5625 void SurgeGUIEditor::resizeWindow(float zf) { setZoomFactor(zf, true); }
5626 
setZoomFactor(float zf)5627 void SurgeGUIEditor::setZoomFactor(float zf) { setZoomFactor(zf, false); }
5628 
setZoomFactor(float zf,bool resizeWindow)5629 void SurgeGUIEditor::setZoomFactor(float zf, bool resizeWindow)
5630 {
5631 #if TARGET_JUCE_UI
5632     zoomFactor = zf;
5633     if (currentSkin && resizeWindow)
5634         parentEd->setSize(currentSkin->getWindowSizeX(), currentSkin->getWindowSizeY());
5635     parentEd->setScaleFactor(zf * 0.01);
5636 #else
5637 
5638     if (zf < minimumZoom)
5639     {
5640         zf = minimumZoom;
5641         showMinimumZoomError();
5642     }
5643 
5644     CRect screenDim = Surge::GUI::getScreenDimensions(getFrame());
5645 
5646     /*
5647     ** If getScreenDimensions() can't determine a size on all platforms it now
5648     ** returns a size 0 screen. In that case we will skip the min check but
5649     ** need to remember the zoom is invalid
5650     */
5651     if (screenDim.getWidth() == 0 || screenDim.getHeight() == 0)
5652     {
5653         zoomInvalid = true;
5654     }
5655 
5656     float newZf;
5657     if (doesZoomFitToScreen(zf, newZf))
5658     {
5659         zoomFactor = zf;
5660     }
5661     else
5662     {
5663         zoomFactor = newZf;
5664         showTooLargeZoomError(screenDim.getWidth(), screenDim.getHeight(), zoomFactor);
5665     }
5666 
5667     /*
5668      * Fixed zoom: zoom factor constraint
5669      */
5670     if (currentSkin && currentSkin->hasFixedZooms())
5671     {
5672         int nzf = 100;
5673         for (auto z : currentSkin->getFixedZooms())
5674         {
5675             if (z <= zoomFactor)
5676                 nzf = z;
5677         }
5678         zoomFactor = nzf;
5679     }
5680 
5681     // update zoom level stored in DAW extra state
5682     synth->storage.getPatch().dawExtraState.editor.instanceZoomFactor = zoomFactor;
5683 
5684     zoom_callback(this, resizeWindow);
5685 
5686     setBitmapZoomFactor(zoomFactor);
5687 #endif
5688 }
5689 
setBitmapZoomFactor(float zf)5690 void SurgeGUIEditor::setBitmapZoomFactor(float zf)
5691 {
5692     float dbs = Surge::GUI::getDisplayBackingScaleFactor(getFrame());
5693     int fullPhysicalZoomFactor = (int)(zf * dbs);
5694     if (bitmapStore != nullptr)
5695         bitmapStore->setPhysicalZoomFactor(fullPhysicalZoomFactor);
5696 }
5697 
showMinimumZoomError() const5698 void SurgeGUIEditor::showMinimumZoomError() const
5699 {
5700     std::ostringstream oss;
5701     oss << "The smallest zoom level possible on your platform is " << minimumZoom
5702         << "%. Sorry, you cannot make Surge any smaller!";
5703     Surge::UserInteractions::promptError(oss.str(), "Zoom Level Error");
5704 }
5705 
showTooLargeZoomError(double width,double height,float zf) const5706 void SurgeGUIEditor::showTooLargeZoomError(double width, double height, float zf) const
5707 {
5708 #if !LINUX
5709     std::ostringstream msg;
5710     msg << "Surge adjusts the maximum zoom level in order to prevent the interface becoming larger "
5711            "than available screen area. "
5712         << "Your screen resolution is " << width << "x" << height << " "
5713         << "for which the target zoom level of " << zf << "% would be too large." << std::endl
5714         << std::endl;
5715     if (currentSkin && currentSkin->hasFixedZooms())
5716     {
5717         msg << "Surge chose the largest fitting fixed zoom which is provided by this skin.";
5718     }
5719     else
5720     {
5721         msg << "Surge chose the largest fitting zoom level of " << zf << "%.";
5722     }
5723     Surge::UserInteractions::promptError(msg.str(), "Zoom Level Adjusted");
5724 #endif
5725 }
5726 
showSettingsMenu(CRect & menuRect)5727 void SurgeGUIEditor::showSettingsMenu(CRect &menuRect)
5728 {
5729     COptionMenu *settingsMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
5730                                                 VSTGUI::COptionMenu::kNoDrawStyle |
5731                                                     VSTGUI::COptionMenu::kMultipleCheckStyle);
5732 
5733     int eid = 0;
5734 
5735     auto mpeSubMenu = makeMpeMenu(menuRect, false);
5736     settingsMenu->addEntry(mpeSubMenu, Surge::UI::toOSCaseForMenu("MPE Options"));
5737     eid++;
5738     mpeSubMenu->forget();
5739 
5740     auto tuningSubMenu = makeTuningMenu(menuRect, false);
5741     settingsMenu->addEntry(tuningSubMenu, "Tuning");
5742     eid++;
5743     tuningSubMenu->forget();
5744 
5745     auto zoomMenu = makeZoomMenu(menuRect, false);
5746     settingsMenu->addEntry(zoomMenu, "Zoom");
5747     eid++;
5748     zoomMenu->forget();
5749 
5750     auto skinSubMenu = makeSkinMenu(menuRect);
5751     settingsMenu->addEntry(skinSubMenu, "Skins");
5752     eid++;
5753     skinSubMenu->forget();
5754 
5755     auto uiOptionsMenu = makeUserSettingsMenu(menuRect);
5756     settingsMenu->addEntry(uiOptionsMenu, Surge::UI::toOSCaseForMenu("User Settings"));
5757     uiOptionsMenu->forget();
5758     eid++;
5759 
5760     auto dataSubMenu = makeDataMenu(menuRect);
5761     settingsMenu->addEntry(dataSubMenu, Surge::UI::toOSCaseForMenu("Data Folders"));
5762     eid++;
5763     dataSubMenu->forget();
5764 
5765     auto midiSubMenu = makeMidiMenu(menuRect);
5766     settingsMenu->addEntry(midiSubMenu, Surge::UI::toOSCaseForMenu("MIDI Settings"));
5767     eid++;
5768     midiSubMenu->forget();
5769 
5770     if (useDevMenu)
5771     {
5772         auto devSubMenu = makeDevMenu(menuRect);
5773         settingsMenu->addEntry(devSubMenu, Surge::UI::toOSCaseForMenu("Developer Options"));
5774         eid++;
5775         devSubMenu->forget();
5776     }
5777 
5778     settingsMenu->addSeparator(eid++);
5779 
5780     addCallbackMenu(settingsMenu, Surge::UI::toOSCaseForMenu("Reach the Developers..."), []() {
5781         Surge::UserInteractions::openURL("https://surge-synthesizer.github.io/feedback");
5782     });
5783     eid++;
5784 
5785     addCallbackMenu(settingsMenu, Surge::UI::toOSCaseForMenu("Read the Code..."), []() {
5786         Surge::UserInteractions::openURL("https://github.com/surge-synthesizer/surge/");
5787     });
5788     eid++;
5789 
5790     addCallbackMenu(
5791         settingsMenu, Surge::UI::toOSCaseForMenu("Download Additional Content..."), []() {
5792             Surge::UserInteractions::openURL("https://github.com/surge-synthesizer/"
5793                                              "surge-synthesizer.github.io/wiki/Additional-Content");
5794         });
5795     eid++;
5796 
5797     addCallbackMenu(settingsMenu, Surge::UI::toOSCaseForMenu("Surge Website..."), []() {
5798         Surge::UserInteractions::openURL("https://surge-synthesizer.github.io/");
5799     });
5800     eid++;
5801 
5802     addCallbackMenu(settingsMenu, Surge::UI::toOSCaseForMenu("Surge Manual..."), []() {
5803         Surge::UserInteractions::openURL("https://surge-synthesizer.github.io/manual/");
5804     });
5805     eid++;
5806 
5807     settingsMenu->addSeparator(eid++);
5808 
5809     addCallbackMenu(settingsMenu, "About Surge", [this]() { this->showAboutBox(); });
5810     eid++;
5811 
5812     frame->addView(settingsMenu);
5813     settingsMenu->setDirty();
5814     settingsMenu->popup();
5815     frame->removeView(settingsMenu, true);
5816 }
5817 
makeLfoMenu(VSTGUI::CRect & menuRect)5818 VSTGUI::COptionMenu *SurgeGUIEditor::makeLfoMenu(VSTGUI::CRect &menuRect)
5819 {
5820     int currentLfoId = modsource_editor[current_scene] - ms_lfo1;
5821 
5822     int shapev = synth->storage.getPatch().scene[current_scene].lfo[currentLfoId].shape.val.i;
5823     std::string what = "LFO";
5824     if (lt_mseg == shapev)
5825         what = "MSEG";
5826     if (lt_stepseq == shapev)
5827         what = "Step Seq";
5828     if (lt_envelope == shapev)
5829         what = "Envelope";
5830     if (lt_function == shapev)
5831         // TODO FIXME: When function LFO type is added, adjust this condition!
5832         what = "Envelope";
5833 
5834     auto msurl = SurgeGUIEditor::helpURLForSpecial("lfo-presets");
5835     auto hurl = SurgeGUIEditor::fullyResolvedHelpURL(msurl);
5836 
5837     COptionMenu *lfoSubMenu =
5838         new COptionMenu(menuRect, 0, 0, 0, 0, VSTGUI::COptionMenu::kNoDrawStyle);
5839     addCallbackMenu(lfoSubMenu, "[?] LFO Presets",
5840                     [hurl]() { Surge::UserInteractions::openURL(hurl); }),
5841 
5842         lfoSubMenu->addSeparator();
5843 
5844     addCallbackMenu(lfoSubMenu, Surge::UI::toOSCaseForMenu("Save " + what + " As..."),
5845                     [this, currentLfoId, what]() {
5846                         // Prompt for a name
5847                         promptForMiniEdit("preset", "Enter the name for " + what + " preset:",
5848                                           what + " Preset Name", CPoint(-1, -1),
5849                                           [this, currentLfoId](const std::string &s) {
5850                                               Surge::ModulatorPreset::savePresetToUser(
5851                                                   string_to_path(s), &(this->synth->storage),
5852                                                   current_scene, currentLfoId);
5853                                           });
5854                         // and save
5855                     });
5856 
5857     auto presetCategories = Surge::ModulatorPreset::getPresets(&(synth->storage));
5858 
5859     if (!presetCategories.empty())
5860         lfoSubMenu->addSeparator();
5861 
5862     std::unordered_map<std::string, std::pair<COptionMenu *, bool>> subMenuMaps;
5863     subMenuMaps[""] = std::make_pair(lfoSubMenu, true);
5864     for (auto const &cat : presetCategories)
5865     {
5866         COptionMenu *catSubMenu =
5867             new COptionMenu(menuRect, 0, 0, 0, 0, VSTGUI::COptionMenu::kNoDrawStyle);
5868         subMenuMaps[cat.path] = std::make_pair(catSubMenu, false);
5869     }
5870     for (auto const &cat : presetCategories)
5871     {
5872         if (subMenuMaps.find(cat.path) == subMenuMaps.end())
5873         {
5874             // should never happen
5875             continue;
5876         }
5877         auto catSubMenu = subMenuMaps[cat.path].first;
5878 
5879         auto parentMenu = lfoSubMenu;
5880         if (subMenuMaps.find(cat.parentPath) != subMenuMaps.end())
5881         {
5882             parentMenu = subMenuMaps[cat.parentPath].first;
5883             if (!subMenuMaps[cat.parentPath].second)
5884             {
5885                 subMenuMaps[cat.parentPath].second = true;
5886             }
5887         }
5888 
5889         auto catname = cat.name;
5890 
5891 #if WINDOWS
5892         Surge::Storage::findReplaceSubstring(catname, std::string("&"), std::string("&&"));
5893 #endif
5894 
5895         parentMenu->addEntry(catSubMenu, catname.c_str());
5896         catSubMenu->forget();
5897     }
5898 
5899     for (auto const &cat : presetCategories)
5900     {
5901         if (subMenuMaps.find(cat.path) == subMenuMaps.end())
5902         {
5903             // should never happen
5904             continue;
5905         }
5906 
5907         auto catSubMenu = subMenuMaps[cat.path].first;
5908 
5909         if (catSubMenu->getNbEntries() > 0 && cat.presets.size() > 0)
5910         {
5911             catSubMenu->addSeparator();
5912         }
5913 
5914         for (auto const &p : cat.presets)
5915         {
5916             auto pname = p.name;
5917 
5918 #if WINDOWS
5919             Surge::Storage::findReplaceSubstring(pname, std::string("&"), std::string("&&"));
5920 #endif
5921 
5922             addCallbackMenu(catSubMenu, pname, [this, p, currentLfoId]() {
5923                 Surge::ModulatorPreset::loadPresetFrom(p.path, &(this->synth->storage),
5924                                                        current_scene, currentLfoId);
5925 
5926                 auto newshape = this->synth->storage.getPatch()
5927                                     .scene[current_scene]
5928                                     .lfo[currentLfoId]
5929                                     .shape.val.i;
5930 
5931                 if (isAnyOverlayPresent(MSEG_EDITOR))
5932                 {
5933                     closeMSEGEditor();
5934                     if (newshape == lt_mseg)
5935                         showMSEGEditor();
5936                 }
5937 
5938                 this->synth->refresh_editor = true;
5939             });
5940         }
5941     }
5942 
5943     lfoSubMenu->addSeparator();
5944 
5945     addCallbackMenu(lfoSubMenu, Surge::UI::toOSCaseForMenu("Rescan Presets"),
5946                     []() { Surge::ModulatorPreset::forcePresetRescan(); });
5947     return lfoSubMenu;
5948 }
5949 
makeMpeMenu(VSTGUI::CRect & menuRect,bool showhelp)5950 VSTGUI::COptionMenu *SurgeGUIEditor::makeMpeMenu(VSTGUI::CRect &menuRect, bool showhelp)
5951 {
5952 
5953     COptionMenu *mpeSubMenu =
5954         new COptionMenu(menuRect, 0, 0, 0, 0, VSTGUI::COptionMenu::kNoDrawStyle);
5955 
5956     auto hu = helpURLForSpecial("mpe-menu");
5957     if (hu != "" && showhelp)
5958     {
5959         auto lurl = fullyResolvedHelpURL(hu);
5960         addCallbackMenu(mpeSubMenu, "[?] MPE",
5961                         [lurl]() { Surge::UserInteractions::openURL(lurl); });
5962         mpeSubMenu->addSeparator();
5963     }
5964 
5965     std::string endis = "Enable MPE";
5966     if (synth->mpeEnabled)
5967         endis = "Disable MPE";
5968     addCallbackMenu(mpeSubMenu, endis.c_str(),
5969                     [this]() { this->synth->mpeEnabled = !this->synth->mpeEnabled; });
5970     mpeSubMenu->addSeparator();
5971 
5972     std::ostringstream oss;
5973     oss << "Change MPE Pitch Bend Range (Current: " << synth->storage.mpePitchBendRange
5974         << " Semitones)";
5975     addCallbackMenu(mpeSubMenu, Surge::UI::toOSCaseForMenu(oss.str().c_str()), [this, menuRect]() {
5976         // FIXME! This won't work on linux
5977         const auto c{std::to_string(int(synth->storage.mpePitchBendRange))};
5978         promptForMiniEdit(c, "Enter new MPE pitch bend range:", "MPE Pitch Bend Range",
5979                           menuRect.getTopLeft(), [this](const std::string &c) {
5980                               int newVal = ::atoi(c.c_str());
5981                               this->synth->storage.mpePitchBendRange = newVal;
5982                           });
5983     });
5984 
5985     std::ostringstream oss2;
5986     int def = Surge::Storage::getUserDefaultValue(&(synth->storage), "mpePitchBendRange", 48);
5987     oss2 << "Change Default MPE Pitch Bend Range (Current: " << def << " Semitones)";
5988     addCallbackMenu(mpeSubMenu, Surge::UI::toOSCaseForMenu(oss2.str().c_str()), [this, menuRect]() {
5989         // FIXME! This won't work on linux
5990         const auto c{std::to_string(int(synth->storage.mpePitchBendRange))};
5991         promptForMiniEdit(c, "Enter default MPE pitch bend range:", "Default MPE Pitch Bend Range",
5992                           menuRect.getTopLeft(), [this](const std::string &s) {
5993                               int newVal = ::atoi(s.c_str());
5994                               Surge::Storage::updateUserDefaultValue(&(this->synth->storage),
5995                                                                      "mpePitchBendRange", newVal);
5996                               this->synth->storage.mpePitchBendRange = newVal;
5997                           });
5998     });
5999 
6000     auto men = makeSmoothMenu(menuRect, "pitchSmoothingMode",
6001                               (int)ControllerModulationSource::SmoothingMode::DIRECT,
6002                               [this](auto md) { this->resetPitchSmoothing(md); });
6003     mpeSubMenu->addEntry(men, Surge::UI::toOSCaseForMenu("MPE Pitch Bend Smoothing"));
6004     men->forget();
6005 
6006     return mpeSubMenu;
6007 }
6008 
makeMonoModeOptionsMenu(VSTGUI::CRect & menuRect,bool updateDefaults)6009 VSTGUI::COptionMenu *SurgeGUIEditor::makeMonoModeOptionsMenu(VSTGUI::CRect &menuRect,
6010                                                              bool updateDefaults)
6011 {
6012     COptionMenu *monoSubMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
6013                                                VSTGUI::COptionMenu::kNoDrawStyle |
6014                                                    VSTGUI::COptionMenu::kMultipleCheckStyle);
6015 
6016     auto mode = synth->storage.monoPedalMode;
6017     if (updateDefaults)
6018         mode = (MonoPedalMode)Surge::Storage::getUserDefaultValue(
6019             &(this->synth->storage), "monoPedalMode", (int)HOLD_ALL_NOTES);
6020 
6021     auto cb = addCallbackMenu(
6022         monoSubMenu,
6023         Surge::UI::toOSCaseForMenu("Sustain Pedal Holds All Notes (No Note Off Retrigger)"),
6024         [this, updateDefaults]() {
6025             this->synth->storage.monoPedalMode = HOLD_ALL_NOTES;
6026             if (updateDefaults)
6027                 Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "monoPedalMode",
6028                                                        (int)HOLD_ALL_NOTES);
6029         });
6030     if (mode == HOLD_ALL_NOTES)
6031         cb->setChecked(true);
6032 
6033     cb = addCallbackMenu(
6034         monoSubMenu, Surge::UI::toOSCaseForMenu("Sustain Pedal Allows Note Off Retrigger"),
6035         [this, updateDefaults]() {
6036             this->synth->storage.monoPedalMode = RELEASE_IF_OTHERS_HELD;
6037             if (updateDefaults)
6038                 Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "monoPedalMode",
6039                                                        (int)RELEASE_IF_OTHERS_HELD);
6040         });
6041     if (mode == RELEASE_IF_OTHERS_HELD)
6042         cb->setChecked(true);
6043 
6044     return monoSubMenu;
6045 }
6046 
makeTuningMenu(VSTGUI::CRect & menuRect,bool showhelp)6047 VSTGUI::COptionMenu *SurgeGUIEditor::makeTuningMenu(VSTGUI::CRect &menuRect, bool showhelp)
6048 {
6049     int tid = 0;
6050     COptionMenu *tuningSubMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
6051                                                  VSTGUI::COptionMenu::kNoDrawStyle |
6052                                                      VSTGUI::COptionMenu::kMultipleCheckStyle);
6053 
6054     auto hu = helpURLForSpecial("tun-menu");
6055 
6056     if (hu != "" && showhelp)
6057     {
6058         auto lurl = fullyResolvedHelpURL(hu);
6059         addCallbackMenu(tuningSubMenu, "[?] Tuning ",
6060                         [lurl]() { Surge::UserInteractions::openURL(lurl); });
6061         tid++;
6062         tuningSubMenu->addSeparator();
6063     }
6064 
6065     bool isTuningEnabled = !synth->storage.isStandardTuning;
6066     bool isScaleEnabled = !synth->storage.isStandardScale;
6067     bool isMappingEnabled = !synth->storage.isStandardMapping;
6068 
6069     if (isScaleEnabled)
6070     {
6071         auto tuningLabel = Surge::UI::toOSCaseForMenu("Current Tuning: ");
6072 
6073         if (synth->storage.currentScale.description.empty())
6074         {
6075             tuningLabel += path_to_string(fs::path(synth->storage.currentScale.name).stem());
6076         }
6077         else
6078         {
6079             tuningLabel += synth->storage.currentScale.description;
6080         }
6081         auto curTuning = new CCommandMenuItem(CCommandMenuItem::Desc(tuningLabel.c_str()));
6082         curTuning->setEnabled(false);
6083         tuningSubMenu->addEntry(curTuning);
6084         tid++;
6085     }
6086 
6087     if (isMappingEnabled)
6088     {
6089         auto mappingLabel = Surge::UI::toOSCaseForMenu("Current Keyboard Mapping: ");
6090         mappingLabel += path_to_string(fs::path(synth->storage.currentMapping.name).stem());
6091         auto curMapping = new CCommandMenuItem(CCommandMenuItem::Desc(mappingLabel.c_str()));
6092         curMapping->setEnabled(false);
6093         tuningSubMenu->addEntry(curMapping);
6094         tid++;
6095     }
6096 
6097     if (isTuningEnabled || isMappingEnabled)
6098     {
6099         tuningSubMenu->addSeparator();
6100         tid++;
6101     }
6102 
6103     auto *st = addCallbackMenu(tuningSubMenu, Surge::UI::toOSCaseForMenu("Set to Standard Tuning"),
6104                                [this]() {
6105                                    this->synth->storage.retuneTo12TETScaleC261Mapping();
6106                                    this->synth->refresh_editor = true;
6107                                });
6108     st->setEnabled(!this->synth->storage.isStandardTuning);
6109     tid++;
6110 
6111     st = addCallbackMenu(tuningSubMenu,
6112                          Surge::UI::toOSCaseForMenu("Set to Standard Scale (12-TET)"), [this]() {
6113                              this->synth->storage.retuneTo12TETScale();
6114                              this->synth->refresh_editor = true;
6115                          });
6116     st->setEnabled(!this->synth->storage.isStandardScale);
6117     tid++;
6118     auto *kst = addCallbackMenu(
6119         tuningSubMenu, Surge::UI::toOSCaseForMenu("Set to Standard Mapping (Concert C)"), [this]() {
6120             this->synth->storage.remapToConcertCKeyboard();
6121             this->synth->refresh_editor = true;
6122         });
6123     kst->setEnabled(!this->synth->storage.isStandardMapping);
6124     tid++;
6125 
6126     tuningSubMenu->addSeparator();
6127     tid++;
6128 
6129     addCallbackMenu(tuningSubMenu, Surge::UI::toOSCaseForMenu("Load .scl Scale..."), [this]() {
6130         auto cb = [this](std::string sf) {
6131             std::string sfx = ".scl";
6132             if (sf.length() >= sfx.length())
6133             {
6134                 if (sf.compare(sf.length() - sfx.length(), sfx.length(), sfx) != 0)
6135                 {
6136                     Surge::UserInteractions::promptError("Please select only .scl files!",
6137                                                          "Invalid Choice");
6138                     std::cout << "FILE is [" << sf << "]" << std::endl;
6139                     return;
6140                 }
6141             }
6142             try
6143             {
6144                 auto sc = Tunings::readSCLFile(sf);
6145 
6146                 if (!this->synth->storage.retuneToScale(sc))
6147                 {
6148                     Surge::UserInteractions::promptError("This .scl file is not valid!",
6149                                                          "File Format Error");
6150                     return;
6151                 }
6152                 this->synth->refresh_editor = true;
6153             }
6154             catch (Tunings::TuningError &e)
6155             {
6156                 Surge::UserInteractions::promptError(e.what(), "Loading Error");
6157             }
6158         };
6159 
6160         auto scl_path =
6161             Surge::Storage::appendDirectory(this->synth->storage.datapath, "tuning-library", "SCL");
6162 
6163         Surge::UserInteractions::promptFileOpenDialog(scl_path, ".scl",
6164                                                       "Scala microtuning files (*.scl)", cb);
6165     });
6166     tid++;
6167 
6168     addCallbackMenu(
6169         tuningSubMenu, Surge::UI::toOSCaseForMenu("Load .kbm Keyboard Mapping..."), [this]() {
6170             auto cb = [this](std::string sf) {
6171                 std::string sfx = ".kbm";
6172                 if (sf.length() >= sfx.length())
6173                 {
6174                     if (sf.compare(sf.length() - sfx.length(), sfx.length(), sfx) != 0)
6175                     {
6176                         Surge::UserInteractions::promptError("Please select only .kbm files!",
6177                                                              "Invalid Choice");
6178                         std::cout << "FILE is [" << sf << "]" << std::endl;
6179                         return;
6180                     }
6181                 }
6182                 try
6183                 {
6184                     auto kb = Tunings::readKBMFile(sf);
6185 
6186                     if (!this->synth->storage.remapToKeyboard(kb))
6187                     {
6188                         Surge::UserInteractions::promptError("This .kbm file is not valid!",
6189                                                              "File Format Error");
6190                         return;
6191                     }
6192 
6193                     this->synth->refresh_editor = true;
6194                 }
6195                 catch (Tunings::TuningError &e)
6196                 {
6197                     Surge::UserInteractions::promptError(e.what(), "Loading Error");
6198                 }
6199             };
6200 
6201             auto kbm_path = Surge::Storage::appendDirectory(this->synth->storage.datapath,
6202                                                             "tuning-library", "KBM Concert Pitch");
6203 
6204             Surge::UserInteractions::promptFileOpenDialog(
6205                 kbm_path, ".kbm", "Scala keyboard mapping files (*.kbm)", cb);
6206         });
6207     tid++;
6208 
6209     int oct = 5 - Surge::Storage::getUserDefaultValue(&(this->synth->storage), "middleC", 1);
6210     string middle_A = "A" + to_string(oct);
6211 
6212     addCallbackMenu(
6213         tuningSubMenu,
6214         Surge::UI::toOSCaseForMenu("Remap " + middle_A + " (MIDI Note 69) Directly to..."),
6215         [this, middle_A, menuRect]() {
6216             char ma[256];
6217             sprintf(ma, "Remap %s Frequency", middle_A.c_str());
6218 
6219             char c[256];
6220             snprintf(c, 256, "440.0");
6221             promptForMiniEdit(c, "Remap MIDI note 69 frequency to: ", ma, menuRect.getTopLeft(),
6222                               [this](const std::string &s) {
6223                                   float freq = ::atof(s.c_str());
6224                                   auto kb = Tunings::tuneA69To(freq);
6225                                   kb.name = "Note 69 Retuned 440 to " + std::to_string(freq);
6226                                   if (!this->synth->storage.remapToKeyboard(kb))
6227                                   {
6228                                       Surge::UserInteractions::promptError(
6229                                           "This .kbm file is not valid!", "File Format Error");
6230                                       return;
6231                                   }
6232                               });
6233         });
6234 
6235     tuningSubMenu->addSeparator();
6236 
6237     auto mod = addCallbackMenu(
6238         tuningSubMenu, Surge::UI::toOSCaseForMenu("Apply Tuning at MIDI Input"), [this]() {
6239             this->synth->storage.setTuningApplicationMode(SurgeStorage::RETUNE_MIDI_ONLY);
6240         });
6241     mod->setChecked(synth->storage.tuningApplicationMode == SurgeStorage::RETUNE_MIDI_ONLY);
6242     tid++;
6243 
6244     mod = addCallbackMenu(
6245         tuningSubMenu, Surge::UI::toOSCaseForMenu("Apply Tuning After Modulation"),
6246         [this]() { this->synth->storage.setTuningApplicationMode(SurgeStorage::RETUNE_ALL); });
6247     mod->setChecked(synth->storage.tuningApplicationMode == SurgeStorage::RETUNE_ALL);
6248     tid++;
6249 
6250     tuningSubMenu->addSeparator();
6251 
6252     bool tsMode = Surge::Storage::getUserDefaultValue(&(this->synth->storage), "useODDMTS", false);
6253     std::string txt = "Use ODDSound" + Surge::UI::toOSCaseForMenu(" MTS-ESP (if Loaded in DAW)");
6254 
6255     auto menuItem = addCallbackMenu(tuningSubMenu, txt, [this, tsMode]() {
6256         Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "useODDMTS", !tsMode);
6257         if (tsMode)
6258         {
6259             // We toggled to false
6260             this->synth->storage.deinitialize_oddsound();
6261         }
6262         else
6263         {
6264             this->synth->storage.initialize_oddsound();
6265         }
6266     });
6267 
6268     if (tsMode && !this->synth->storage.oddsound_mts_client)
6269     {
6270         addCallbackMenu(tuningSubMenu, Surge::UI::toOSCaseForMenu("Reconnect to MTS-ESP"),
6271                         [this]() { this->synth->storage.initialize_oddsound(); });
6272     }
6273     menuItem->setChecked(tsMode);
6274 
6275     if (this->synth->storage.oddsound_mts_active && this->synth->storage.oddsound_mts_client)
6276     {
6277         // a 'turn MTS off' menu
6278         addCallbackMenu(tuningSubMenu, Surge::UI::toOSCaseForMenu("Disconnect from MTS-ESP"),
6279                         [this]() {
6280                             auto q = this->synth->storage.oddsound_mts_client;
6281                             this->synth->storage.oddsound_mts_active = false;
6282                             this->synth->storage.oddsound_mts_client = nullptr;
6283                             MTS_DeregisterClient(q);
6284                         });
6285 
6286         // an MTS tuning toggle
6287         auto mm = addCallbackMenu(
6288             tuningSubMenu, Surge::UI::toOSCaseForMenu("Query Tuning at Note On Only"), [this]() {
6289                 if (this->synth->storage.oddsoundRetuneMode == SurgeStorage::RETUNE_CONSTANT)
6290                 {
6291                     this->synth->storage.oddsoundRetuneMode = SurgeStorage::RETUNE_NOTE_ON_ONLY;
6292                 }
6293                 else
6294                 {
6295                     this->synth->storage.oddsoundRetuneMode = SurgeStorage::RETUNE_CONSTANT;
6296                 }
6297             });
6298         mm->setEnabled(true);
6299         mm->setChecked(this->synth->storage.oddsoundRetuneMode ==
6300                        SurgeStorage::RETUNE_NOTE_ON_ONLY);
6301 
6302         // an 'MTS is on' disabled menu
6303         mm = addCallbackMenu(tuningSubMenu, Surge::UI::toOSCaseForMenu("MTS-ESP is Active"),
6304                              []() {});
6305         mm->setEnabled(false);
6306 
6307         // an 'MTS scale name' disabled menu
6308         std::string mtsScale = MTS_GetScaleName(synth->storage.oddsound_mts_client);
6309         mm = addCallbackMenu(tuningSubMenu, mtsScale, []() {});
6310         mm->setEnabled(false);
6311 
6312         return tuningSubMenu;
6313     }
6314 
6315     tuningSubMenu->addSeparator();
6316 
6317     tid++;
6318     auto *sct = addCallbackMenu(
6319         tuningSubMenu, Surge::UI::toOSCaseForMenu("Show Current Tuning Information..."),
6320         [this]() { Surge::UserInteractions::showHTML(this->tuningToHtml()); });
6321     sct->setEnabled(!this->synth->storage.isStandardTuning);
6322 
6323     addCallbackMenu(
6324         tuningSubMenu, Surge::UI::toOSCaseForMenu("Factory Tuning Library..."), [this]() {
6325             auto dpath =
6326                 Surge::Storage::appendDirectory(this->synth->storage.datapath, "tuning-library");
6327 
6328             Surge::UserInteractions::openFolderInFileBrowser(dpath);
6329         });
6330 
6331     return tuningSubMenu;
6332 }
6333 
makeZoomMenu(VSTGUI::CRect & menuRect,bool showhelp)6334 VSTGUI::COptionMenu *SurgeGUIEditor::makeZoomMenu(VSTGUI::CRect &menuRect, bool showhelp)
6335 {
6336     COptionMenu *zoomSubMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
6337                                                VSTGUI::COptionMenu::kNoDrawStyle |
6338                                                    VSTGUI::COptionMenu::kMultipleCheckStyle);
6339 
6340     int zid = 0;
6341 
6342     auto hu = helpURLForSpecial("zoom-menu");
6343     if (hu != "" && showhelp)
6344     {
6345         auto lurl = fullyResolvedHelpURL(hu);
6346         addCallbackMenu(zoomSubMenu, "[?] Zoom",
6347                         [lurl]() { Surge::UserInteractions::openURL(lurl); });
6348         zid++;
6349 
6350         zoomSubMenu->addSeparator(zid++);
6351     }
6352 
6353     std::vector<int> zoomTos = {{100, 125, 150, 175, 200, 300, 400}};
6354     bool isFixed = false;
6355     if (currentSkin->hasFixedZooms())
6356     {
6357         isFixed = true;
6358         zoomTos = currentSkin->getFixedZooms();
6359     }
6360 
6361     for (auto s : zoomTos) // These are somewhat arbitrary reasonable defaults
6362     {
6363         std::ostringstream lab;
6364         lab << "Zoom to " << s << "%";
6365         CCommandMenuItem *zcmd = new CCommandMenuItem(CCommandMenuItem::Desc(lab.str()));
6366         zcmd->setActions([this, s](CCommandMenuItem *m) { resizeWindow(s); });
6367         zoomSubMenu->addEntry(zcmd);
6368         if (s == zoomFactor)
6369             zcmd->setChecked(true);
6370         zid++;
6371     }
6372     zoomSubMenu->addSeparator(zid++);
6373 
6374     if (isFixed)
6375     {
6376         /*
6377          * DO WE WANT SOMETHING LIKE THIS?
6378         addCallbackMenu( zoomSubMenu, Surge::UI::toOSCaseForMenu( "About fixed zoom skins..." ),
6379                          [](){});
6380                          */
6381     }
6382     else
6383     {
6384         for (auto jog : {-25, -10, 10, 25}) // These are somewhat arbitrary reasonable defaults also
6385         {
6386             std::ostringstream lab;
6387             if (jog > 0)
6388                 lab << "Grow by " << jog << "%";
6389             else
6390                 lab << "Shrink by " << -jog << "%";
6391 
6392             CCommandMenuItem *zcmd = new CCommandMenuItem(CCommandMenuItem::Desc(lab.str()));
6393             zcmd->setActions(
6394                 [this, jog](CCommandMenuItem *m) { resizeWindow(getZoomFactor() + jog); });
6395             zoomSubMenu->addEntry(zcmd);
6396             zid++;
6397         }
6398 
6399         zoomSubMenu->addSeparator(zid++);
6400 
6401         CCommandMenuItem *biggestZ = new CCommandMenuItem(
6402             CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Zoom to Largest")));
6403         biggestZ->setActions([this](CCommandMenuItem *m) {
6404             int newZF = findLargestFittingZoomBetween(100.0, 500.0, 5,
6405                                                       90, // See comment in setZoomFactor
6406                                                       getWindowSizeX(), getWindowSizeY());
6407             resizeWindow(newZF);
6408         });
6409         zoomSubMenu->addEntry(biggestZ);
6410         zid++;
6411 
6412         CCommandMenuItem *smallestZ = new CCommandMenuItem(
6413             CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Zoom to Smallest")));
6414         smallestZ->setActions([this](CCommandMenuItem *m) { resizeWindow(minimumZoom); });
6415         zoomSubMenu->addEntry(smallestZ);
6416         zid++;
6417 
6418         zoomSubMenu->addSeparator(zid++);
6419 
6420         auto dzf =
6421             Surge::Storage::getUserDefaultValue(&(synth->storage), "defaultZoom", zoomFactor);
6422         std::ostringstream dss;
6423         dss << "Zoom to Default (" << dzf << "%)";
6424         CCommandMenuItem *todefaultZ = new CCommandMenuItem(
6425             CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu(dss.str().c_str())));
6426         todefaultZ->setActions([this, dzf](CCommandMenuItem *m) { resizeWindow(dzf); });
6427         zoomSubMenu->addEntry(todefaultZ);
6428         zid++;
6429     }
6430 
6431     CCommandMenuItem *defaultZ = new CCommandMenuItem(
6432         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Set Current Zoom Level as Default")));
6433     defaultZ->setActions([this](CCommandMenuItem *m) {
6434         Surge::Storage::updateUserDefaultValue(&(synth->storage), "defaultZoom", zoomFactor);
6435     });
6436     zoomSubMenu->addEntry(defaultZ);
6437     zid++;
6438 
6439     if (!isFixed)
6440     {
6441         addCallbackMenu(zoomSubMenu, Surge::UI::toOSCaseForMenu("Set Default Zoom Level to..."),
6442                         [this, menuRect]() {
6443                             // FIXME! This won't work on linux
6444                             char c[256];
6445                             snprintf(c, 256, "%d", (int)zoomFactor);
6446                             promptForMiniEdit(
6447                                 c, "Enter a default zoom level value:", "Set Default Zoom Level",
6448                                 menuRect.getTopLeft(), [this](const std::string &s) {
6449                                     int newVal = ::atoi(s.c_str());
6450                                     Surge::Storage::updateUserDefaultValue(&(synth->storage),
6451                                                                            "defaultZoom", newVal);
6452                                     resizeWindow(newVal);
6453                                 });
6454                         });
6455         zid++;
6456     }
6457 
6458 #if TARGET_VST3
6459     if (!isFixed)
6460     {
6461         zoomSubMenu->addSeparator(zid++);
6462 
6463         bool dragResize =
6464             Surge::Storage::getUserDefaultValue(&(this->synth->storage), "dragResizeVST3", true);
6465 
6466         auto menuItem = addCallbackMenu(
6467             zoomSubMenu, Surge::UI::toOSCaseForMenu("Resize by Dragging the Bottom Right Corner"),
6468             [this, dragResize]() {
6469                 Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "dragResizeVST3",
6470                                                        !dragResize);
6471             });
6472         menuItem->setChecked(dragResize);
6473 
6474         zid++;
6475     }
6476 #endif
6477 
6478     return zoomSubMenu;
6479 }
6480 
makeUserSettingsMenu(VSTGUI::CRect & menuRect)6481 VSTGUI::COptionMenu *SurgeGUIEditor::makeUserSettingsMenu(VSTGUI::CRect &menuRect)
6482 {
6483     COptionMenu *uiOptionsMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
6484                                                  VSTGUI::COptionMenu::kNoDrawStyle |
6485                                                      VSTGUI::COptionMenu::kMultipleCheckStyle);
6486 
6487 #if WINDOWS
6488 #define SUPPORTS_TOUCH_MENU 1
6489 #else
6490 #define SUPPORTS_TOUCH_MENU 0
6491 #endif
6492 
6493 #if SUPPORTS_TOUCH_MENU
6494     bool touchMode =
6495         Surge::Storage::getUserDefaultValue(&(synth->storage), "touchMouseMode", false);
6496 #else
6497     bool touchMode = false;
6498 #endif
6499 
6500     // Mouse behavior submenu
6501     int mid = 0;
6502     COptionMenu *mouseSubMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
6503                                                 VSTGUI::COptionMenu::kNoDrawStyle |
6504                                                     VSTGUI::COptionMenu::kMultipleCheckStyle);
6505 
6506     std::string mouseLegacy = "Legacy";
6507     std::string mouseSlow = "Slow";
6508     std::string mouseMedium = "Medium";
6509     std::string mouseExact = "Exact";
6510 
6511     VSTGUI::CCommandMenuItem *menuItem = nullptr;
6512 
6513     menuItem = addCallbackMenu(mouseSubMenu, mouseLegacy.c_str(), [this]() {
6514         CSurgeSlider::sliderMoveRateState = CSurgeSlider::kLegacy;
6515         Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "sliderMoveRateState",
6516                                                CSurgeSlider::sliderMoveRateState);
6517     });
6518     menuItem->setChecked((CSurgeSlider::sliderMoveRateState == CSurgeSlider::kLegacy));
6519     menuItem->setEnabled(!touchMode);
6520     mid++;
6521 
6522     menuItem = addCallbackMenu(mouseSubMenu, mouseSlow.c_str(), [this]() {
6523         CSurgeSlider::sliderMoveRateState = CSurgeSlider::kSlow;
6524         Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "sliderMoveRateState",
6525                                                CSurgeSlider::sliderMoveRateState);
6526     });
6527     menuItem->setChecked((CSurgeSlider::sliderMoveRateState == CSurgeSlider::kSlow));
6528     menuItem->setEnabled(!touchMode);
6529     mid++;
6530 
6531     menuItem = addCallbackMenu(mouseSubMenu, mouseMedium.c_str(), [this]() {
6532         CSurgeSlider::sliderMoveRateState = CSurgeSlider::kMedium;
6533         Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "sliderMoveRateState",
6534                                                CSurgeSlider::sliderMoveRateState);
6535     });
6536     menuItem->setChecked((CSurgeSlider::sliderMoveRateState == CSurgeSlider::kMedium));
6537     menuItem->setEnabled(!touchMode);
6538     mid++;
6539 
6540     menuItem = addCallbackMenu(mouseSubMenu, mouseExact.c_str(), [this]() {
6541         CSurgeSlider::sliderMoveRateState = CSurgeSlider::kExact;
6542         Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "sliderMoveRateState",
6543                                                CSurgeSlider::sliderMoveRateState);
6544     });
6545     menuItem->setChecked((CSurgeSlider::sliderMoveRateState == CSurgeSlider::kExact));
6546     menuItem->setEnabled(!touchMode);
6547     mid++;
6548 
6549     mouseSubMenu->addSeparator(mid++);
6550 
6551     bool tsMode = Surge::Storage::getUserDefaultValue(&(this->synth->storage),
6552                                                       "showCursorWhileEditing", true);
6553 
6554     menuItem = addCallbackMenu(
6555         mouseSubMenu, Surge::UI::toOSCaseForMenu("Show Cursor While Editing"), [this, tsMode]() {
6556             Surge::Storage::updateUserDefaultValue(&(this->synth->storage),
6557                                                    "showCursorWhileEditing", !tsMode);
6558         });
6559     menuItem->setChecked(tsMode);
6560     menuItem->setEnabled(!touchMode);
6561 
6562 #if SUPPORTS_TOUCH_MENU
6563     mouseSubMenu->addSeparator();
6564     menuItem = addCallbackMenu(mouseSubMenu, Surge::UI::toOSCaseForMenu("Touchscreen Mode"),
6565                                [this, touchMode]() {
6566                                    Surge::Storage::updateUserDefaultValue(
6567                                        &(this->synth->storage), "touchMouseMode", !touchMode);
6568                                });
6569     menuItem->setChecked(touchMode);
6570 #endif
6571 
6572     std::string mouseMenuName = Surge::UI::toOSCaseForMenu("Mouse Behavior");
6573 
6574     uiOptionsMenu->addEntry(mouseSubMenu, mouseMenuName.c_str());
6575     mouseSubMenu->forget();
6576 
6577     // patch defaults
6578     COptionMenu *patchDefMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
6579                                                 VSTGUI::COptionMenu::kNoDrawStyle |
6580                                                     VSTGUI::COptionMenu::kMultipleCheckStyle);
6581 
6582     VSTGUI::CCommandMenuItem *pdItem = nullptr;
6583 
6584     pdItem = addCallbackMenu(
6585         patchDefMenu, Surge::UI::toOSCaseForMenu("Set Default Patch Author..."),
6586         [this, menuRect]() {
6587             string s = Surge::Storage::getUserDefaultValue(&(this->synth->storage),
6588                                                            "defaultPatchAuthor", "");
6589             char txt[256];
6590             txt[0] = 0;
6591             if (Surge::Storage::isValidUTF8(s))
6592                 strxcpy(txt, s.c_str(), 256);
6593             promptForMiniEdit(txt, "Enter default patch author name:", "Set Default Patch Author",
6594                               menuRect.getTopLeft(), [this](const std::string &s) {
6595                                   Surge::Storage::updateUserDefaultValue(&(this->synth->storage),
6596                                                                          "defaultPatchAuthor", s);
6597                               });
6598         });
6599 
6600     pdItem = addCallbackMenu(
6601         patchDefMenu, Surge::UI::toOSCaseForMenu("Set Default Patch Comment..."),
6602         [this, menuRect]() {
6603             string s = Surge::Storage::getUserDefaultValue(&(this->synth->storage),
6604                                                            "defaultPatchComment", "");
6605             char txt[256];
6606             txt[0] = 0;
6607             if (Surge::Storage::isValidUTF8(s))
6608                 strxcpy(txt, s.c_str(), 256);
6609             promptForMiniEdit(txt, "Enter default patch comment text:", "Set Default Patch Comment",
6610                               menuRect.getTopLeft(), [this](const std::string &s) {
6611                                   Surge::Storage::updateUserDefaultValue(&(this->synth->storage),
6612                                                                          "defaultPatchComment", s);
6613                               });
6614         });
6615 
6616     uiOptionsMenu->addEntry(patchDefMenu, Surge::UI::toOSCaseForMenu("Patch Defaults"));
6617     patchDefMenu->forget();
6618 
6619     auto *dispDefMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
6620                                         VSTGUI::COptionMenu::kNoDrawStyle |
6621                                             VSTGUI::COptionMenu::kMultipleCheckStyle);
6622 
6623     // high precision value readouts
6624     bool precReadout = Surge::Storage::getUserDefaultValue(&(this->synth->storage),
6625                                                            "highPrecisionReadouts", false);
6626 
6627     menuItem =
6628         addCallbackMenu(dispDefMenu, Surge::UI::toOSCaseForMenu("High Precision Value Readouts"),
6629                         [this, precReadout]() {
6630                             Surge::Storage::updateUserDefaultValue(
6631                                 &(this->synth->storage), "highPrecisionReadouts", !precReadout);
6632                         });
6633     menuItem->setChecked(precReadout);
6634 
6635     // modulation value readout shows bounds
6636     bool modValues =
6637         Surge::Storage::getUserDefaultValue(&(this->synth->storage), "modWindowShowsValues", false);
6638 
6639     menuItem = addCallbackMenu(dispDefMenu,
6640                                Surge::UI::toOSCaseForMenu("Modulation Value Readout Shows Bounds"),
6641                                [this, modValues]() {
6642                                    Surge::Storage::updateUserDefaultValue(
6643                                        &(this->synth->storage), "modWindowShowsValues", !modValues);
6644                                });
6645     menuItem->setChecked(modValues);
6646 
6647     // lfoone. I think this is a display thing. But could be workflowalso?
6648     bool lfoone = Surge::Storage::getUserDefaultValue(&(this->synth->storage),
6649                                                       "showGhostedLFOWaveReference", true);
6650 
6651     menuItem = addCallbackMenu(
6652         dispDefMenu, Surge::UI::toOSCaseForMenu("Show Ghosted LFO Waveform Reference"),
6653         [this, lfoone]() {
6654             Surge::Storage::updateUserDefaultValue(&(this->synth->storage),
6655                                                    "showGhostedLFOWaveReference", !lfoone);
6656             this->frame->invalid();
6657         });
6658     menuItem->setChecked(lfoone);
6659 
6660     // Middle C submenu
6661     COptionMenu *middleCSubMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
6662                                                   VSTGUI::COptionMenu::kNoDrawStyle |
6663                                                       VSTGUI::COptionMenu::kMultipleCheckStyle);
6664 
6665     VSTGUI::CCommandMenuItem *mcItem = nullptr;
6666 
6667     auto mcValue = Surge::Storage::getUserDefaultValue(&(this->synth->storage), "middleC", 1);
6668 
6669     mcItem = addCallbackMenu(middleCSubMenu, "C3", [this]() {
6670         Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "middleC", 2);
6671         synth->refresh_editor = true;
6672     });
6673     mcItem->setChecked(mcValue == 2);
6674 
6675     mcItem = addCallbackMenu(middleCSubMenu, "C4", [this]() {
6676         Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "middleC", 1);
6677         synth->refresh_editor = true;
6678     });
6679     mcItem->setChecked(mcValue == 1);
6680 
6681     mcItem = addCallbackMenu(middleCSubMenu, "C5", [this]() {
6682         Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "middleC", 0);
6683         synth->refresh_editor = true;
6684     });
6685     mcItem->setChecked(mcValue == 0);
6686 
6687     dispDefMenu->addEntry(middleCSubMenu, "Middle C");
6688     middleCSubMenu->forget();
6689 
6690     uiOptionsMenu->addEntry(dispDefMenu, Surge::UI::toOSCaseForMenu("Value Displays"));
6691     dispDefMenu->forget();
6692 
6693     auto *wfMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
6694                                    VSTGUI::COptionMenu::kNoDrawStyle |
6695                                        VSTGUI::COptionMenu::kMultipleCheckStyle);
6696 
6697     // activate individual scene outputs
6698     menuItem = addCallbackMenu(
6699         wfMenu, Surge::UI::toOSCaseForMenu("Activate Individual Scene Outputs"), [this]() {
6700             this->synth->activateExtraOutputs = !this->synth->activateExtraOutputs;
6701             Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "activateExtraOutputs",
6702                                                    this->synth->activateExtraOutputs ? 1 : 0);
6703         });
6704     menuItem->setChecked(synth->activateExtraOutputs);
6705 
6706     bool msegSnapMem = Surge::Storage::getUserDefaultValue(&(this->synth->storage),
6707                                                            "restoreMSEGSnapFromPatch", true);
6708 
6709     menuItem =
6710         addCallbackMenu(wfMenu, Surge::UI::toOSCaseForMenu("Load MSEG Snap State from Patch"),
6711                         [this, msegSnapMem]() {
6712                             Surge::Storage::updateUserDefaultValue(
6713                                 &(this->synth->storage), "restoreMSEGSnapFromPatch", !msegSnapMem);
6714                         });
6715     menuItem->setChecked(msegSnapMem);
6716 
6717     // remember tab positions per scene
6718     bool tabPosMem = Surge::Storage::getUserDefaultValue(&(this->synth->storage),
6719                                                          "rememberTabPositionsPerScene", false);
6720 
6721     menuItem = addCallbackMenu(
6722         wfMenu, Surge::UI::toOSCaseForMenu("Remember Tab Positions Per Scene"),
6723         [this, tabPosMem]() {
6724             Surge::Storage::updateUserDefaultValue(&(this->synth->storage),
6725                                                    "rememberTabPositionsPerScene", !tabPosMem);
6726         });
6727     menuItem->setChecked(tabPosMem);
6728 
6729     // wrap around browsing patches within current category
6730     bool patchJogWrap =
6731         Surge::Storage::getUserDefaultValue(&(this->synth->storage), "patchJogWraparound", true);
6732 
6733     menuItem = addCallbackMenu(
6734         wfMenu, Surge::UI::toOSCaseForMenu("Previous/Next Patch Constrained to Current Category"),
6735         [this, patchJogWrap]() {
6736             Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "patchJogWraparound",
6737                                                    !patchJogWrap);
6738         });
6739     menuItem->setChecked(patchJogWrap);
6740 
6741     uiOptionsMenu->addEntry(wfMenu, Surge::UI::toOSCaseForMenu("Workflow"));
6742     wfMenu->forget();
6743 
6744     return uiOptionsMenu;
6745 }
6746 
makeSkinMenu(VSTGUI::CRect & menuRect)6747 VSTGUI::COptionMenu *SurgeGUIEditor::makeSkinMenu(VSTGUI::CRect &menuRect)
6748 {
6749     int tid = 0;
6750     COptionMenu *skinSubMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
6751                                                VSTGUI::COptionMenu::kNoDrawStyle |
6752                                                    VSTGUI::COptionMenu::kMultipleCheckStyle);
6753 
6754     auto &db = Surge::UI::SkinDB::get();
6755     bool hasTests = false;
6756 
6757     // TODO: Later allow nesting
6758     std::map<std::string, std::vector<Surge::UI::SkinDB::Entry>> entryByCategory;
6759     for (auto &entry : db.getAvailableSkins())
6760     {
6761         entryByCategory[entry.category].push_back(entry);
6762     }
6763 
6764     for (auto pr : entryByCategory)
6765     {
6766         auto addToThis = skinSubMenu;
6767         auto cat = pr.first;
6768         if (cat != "")
6769         {
6770             addToThis = new COptionMenu(menuRect, 0, 0, 0, 0,
6771                                         VSTGUI::COptionMenu::kNoDrawStyle |
6772                                             VSTGUI::COptionMenu::kMultipleCheckStyle);
6773         }
6774         for (auto &entry : pr.second)
6775         {
6776             auto dname = entry.displayName;
6777             if (useDevMenu)
6778             {
6779                 dname += " (";
6780                 if (entry.root.find(synth->storage.datapath) != std::string::npos)
6781                 {
6782                     dname += "factory";
6783                 }
6784                 else if (entry.root.find(synth->storage.userDataPath) != std::string::npos)
6785                 {
6786                     dname += "user";
6787                 }
6788                 else
6789                 {
6790                     dname += "other";
6791                 }
6792 
6793                 dname += PATH_SEPARATOR + entry.name + ")";
6794             }
6795 
6796             auto cb = addCallbackMenu(addToThis, dname, [this, entry]() {
6797                 setupSkinFromEntry(entry);
6798                 this->synth->refresh_editor = true;
6799                 Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "defaultSkin",
6800                                                        entry.name);
6801                 Surge::Storage::updateUserDefaultValue(&(this->synth->storage),
6802                                                        "defaultSkinRootType", entry.rootType);
6803             });
6804             cb->setChecked(entry.matchesSkin(currentSkin));
6805             tid++;
6806         }
6807         if (cat != "")
6808         {
6809             skinSubMenu->addEntry(addToThis, cat.c_str());
6810             addToThis->forget();
6811         }
6812     }
6813     skinSubMenu->addSeparator();
6814 
6815     if (useDevMenu)
6816     {
6817         auto f5Value =
6818             Surge::Storage::getUserDefaultValue(&(this->synth->storage), "skinReloadViaF5", 0);
6819         VSTGUI::CCommandMenuItem *valItem = nullptr;
6820 
6821         valItem = addCallbackMenu(
6822             skinSubMenu, Surge::UI::toOSCaseForMenu("Use F5 To Reload Current Skin"),
6823             [this, f5Value]() {
6824                 Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "skinReloadViaF5",
6825                                                        f5Value ? 0 : 1);
6826             });
6827         valItem->setChecked(f5Value == 1);
6828 
6829         tid++;
6830 
6831         int pxres =
6832             Surge::Storage::getUserDefaultValue(&(synth->storage), "layoutGridResolution", 16);
6833 
6834         auto m = std::string("Show Layout Grid (") + std::to_string(pxres) + " px)";
6835 
6836         addCallbackMenu(skinSubMenu, Surge::UI::toOSCaseForMenu(m),
6837                         [this, pxres]() { this->showAboutBox(pxres); });
6838 
6839         tid++;
6840 
6841         addCallbackMenu(skinSubMenu, Surge::UI::toOSCaseForMenu("Change Layout Grid Resolution..."),
6842                         [this, pxres]() {
6843                             this->promptForMiniEdit(
6844                                 std::to_string(pxres),
6845                                 "Enter new resolution:", "Layout Grid Resolution", CPoint(400, 400),
6846                                 [this](const std::string &s) {
6847                                     Surge::Storage::updateUserDefaultValue(&(this->synth->storage),
6848                                                                            "layoutGridResolution",
6849                                                                            std::atoi(s.c_str()));
6850                                 });
6851                         });
6852 
6853         skinSubMenu->addSeparator();
6854     }
6855 
6856     addCallbackMenu(skinSubMenu, Surge::UI::toOSCaseForMenu("Reload Current Skin"), [this]() {
6857         this->bitmapStore.reset(new SurgeBitmaps());
6858         this->bitmapStore->setupBitmapsForFrame(frame);
6859         if (!this->currentSkin->reloadSkin(this->bitmapStore))
6860         {
6861             auto &db = Surge::UI::SkinDB::get();
6862             auto msg =
6863                 std::string(
6864                     "Unable to load skin! Reverting the skin to Surge Classic.\n\nSkin error:\n") +
6865                 db.getAndResetErrorString();
6866             this->currentSkin = db.defaultSkin(&(this->synth->storage));
6867             this->currentSkin->reloadSkin(this->bitmapStore);
6868             Surge::UserInteractions::promptError(msg, "Skin Loading Error");
6869         }
6870 
6871         reloadFromSkin();
6872         this->synth->refresh_editor = true;
6873     });
6874     tid++;
6875 
6876     addCallbackMenu(skinSubMenu, Surge::UI::toOSCaseForMenu("Rescan Skins"), [this]() {
6877         auto r = this->currentSkin->root;
6878         auto n = this->currentSkin->name;
6879 
6880         auto &db = Surge::UI::SkinDB::get();
6881         db.rescanForSkins(&(this->synth->storage));
6882 
6883         // So go find the skin
6884         auto e = db.getEntryByRootAndName(r, n);
6885         if (e.isJust())
6886         {
6887             setupSkinFromEntry(e.fromJust());
6888         }
6889         else
6890         {
6891             setupSkinFromEntry(db.getDefaultSkinEntry());
6892         }
6893         this->synth->refresh_editor = true;
6894     });
6895 
6896     tid++;
6897 
6898     skinSubMenu->addSeparator(tid++);
6899 
6900     addCallbackMenu(skinSubMenu, Surge::UI::toOSCaseForMenu("Open Current Skin Folder..."),
6901                     [this]() {
6902                         Surge::UserInteractions::openFolderInFileBrowser(this->currentSkin->root +
6903                                                                          this->currentSkin->name);
6904                     });
6905     tid++;
6906 
6907     skinSubMenu->addSeparator();
6908 
6909     addCallbackMenu(skinSubMenu, Surge::UI::toOSCaseForMenu("Show Skin Inspector..."),
6910                     [this]() { Surge::UserInteractions::showHTML(skinInspectorHtml()); });
6911 
6912     tid++;
6913 
6914     addCallbackMenu(skinSubMenu, Surge::UI::toOSCaseForMenu("Skin Development Guide..."), []() {
6915         Surge::UserInteractions::openURL("https://surge-synthesizer.github.io/skin-manual.html");
6916     });
6917 
6918     tid++;
6919 
6920     return skinSubMenu;
6921 }
6922 
makeDataMenu(VSTGUI::CRect & menuRect)6923 VSTGUI::COptionMenu *SurgeGUIEditor::makeDataMenu(VSTGUI::CRect &menuRect)
6924 {
6925     int did = 0;
6926     COptionMenu *dataSubMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
6927                                                VSTGUI::COptionMenu::kNoDrawStyle |
6928                                                    VSTGUI::COptionMenu::kMultipleCheckStyle);
6929 
6930     addCallbackMenu(
6931         dataSubMenu, Surge::UI::toOSCaseForMenu("Open Factory Data Folder..."), [this]() {
6932             Surge::UserInteractions::openFolderInFileBrowser(this->synth->storage.datapath);
6933         });
6934     did++;
6935 
6936     addCallbackMenu(dataSubMenu, Surge::UI::toOSCaseForMenu("Open User Data Folder..."), [this]() {
6937         // make it if it isn't there
6938         fs::create_directories(string_to_path(this->synth->storage.userDataPath));
6939         Surge::UserInteractions::openFolderInFileBrowser(this->synth->storage.userDataPath);
6940     });
6941     did++;
6942 
6943     addCallbackMenu(
6944         dataSubMenu, Surge::UI::toOSCaseForMenu("Set Custom User Data Folder..."), [this]() {
6945             auto cb = [this](std::string f) {
6946                 // FIXME - check if f is a path
6947                 this->synth->storage.userDataPath = f;
6948                 Surge::Storage::updateUserDefaultValue(&(this->synth->storage), "userDataPath", f);
6949                 this->synth->storage.refresh_wtlist();
6950                 this->synth->storage.refresh_patchlist();
6951             };
6952             Surge::UserInteractions::promptFileOpenDialog(this->synth->storage.userDataPath, "", "",
6953                                                           cb, true, true);
6954         });
6955     did++;
6956 
6957     dataSubMenu->addSeparator(did++);
6958 
6959     addCallbackMenu(dataSubMenu, Surge::UI::toOSCaseForMenu("Rescan All Data Folders"), [this]() {
6960         this->synth->storage.refresh_wtlist();
6961         this->synth->storage.refresh_patchlist();
6962         this->scannedForMidiPresets = false;
6963         CFxMenu::scanForUserPresets =
6964             true; // that's annoying now I see it side by side. But you know.
6965 
6966         // Rescan for skins
6967         auto r = this->currentSkin->root;
6968         auto n = this->currentSkin->name;
6969 
6970         auto &db = Surge::UI::SkinDB::get();
6971         db.rescanForSkins(&(this->synth->storage));
6972 
6973         // So go find the skin
6974         auto e = db.getEntryByRootAndName(r, n);
6975         if (e.isJust())
6976         {
6977             setupSkinFromEntry(e.fromJust());
6978         }
6979         else
6980         {
6981             setupSkinFromEntry(db.getDefaultSkinEntry());
6982         }
6983 
6984         // Will need to rebuild the FX menu also so...
6985         this->synth->refresh_editor = true;
6986     });
6987 
6988     return dataSubMenu;
6989 }
6990 
6991 // builds a menu for setting controller smoothing, used in ::makeMidiMenu and ::makeMpeMenu
6992 // key is the key given to getUserDefaultValue,
6993 // default is a value to default to,
6994 // setSmooth is a function called to set the smoothing value
makeSmoothMenu(VSTGUI::CRect & menuRect,const std::string & key,int defaultValue,std::function<void (ControllerModulationSource::SmoothingMode)> setSmooth)6995 VSTGUI::COptionMenu *SurgeGUIEditor::makeSmoothMenu(
6996     VSTGUI::CRect &menuRect, const std::string &key, int defaultValue,
6997     std::function<void(ControllerModulationSource::SmoothingMode)> setSmooth)
6998 {
6999     COptionMenu *smoothMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
7000                                               VSTGUI::COptionMenu::kNoDrawStyle |
7001                                                   VSTGUI::COptionMenu::kMultipleCheckStyle);
7002 
7003     int smoothing = Surge::Storage::getUserDefaultValue(&(synth->storage), key, defaultValue);
7004 
7005     auto asmt = [this, smoothMenu, smoothing,
7006                  setSmooth](const char *label, ControllerModulationSource::SmoothingMode md) {
7007         auto me = addCallbackMenu(smoothMenu, label, [setSmooth, md]() { setSmooth(md); });
7008         me->setChecked(smoothing == md);
7009     };
7010     asmt("Legacy", ControllerModulationSource::SmoothingMode::LEGACY);
7011     asmt("Slow Exponential", ControllerModulationSource::SmoothingMode::SLOW_EXP);
7012     asmt("Fast Exponential", ControllerModulationSource::SmoothingMode::FAST_EXP);
7013     asmt("Fast Linear", ControllerModulationSource::SmoothingMode::FAST_LINE);
7014     asmt("No Smoothing", ControllerModulationSource::SmoothingMode::DIRECT);
7015     return smoothMenu;
7016 };
7017 
makeMidiMenu(VSTGUI::CRect & menuRect)7018 VSTGUI::COptionMenu *SurgeGUIEditor::makeMidiMenu(VSTGUI::CRect &menuRect)
7019 {
7020     COptionMenu *midiSubMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
7021                                                VSTGUI::COptionMenu::kNoDrawStyle |
7022                                                    VSTGUI::COptionMenu::kMultipleCheckStyle);
7023 
7024     auto smen = makeSmoothMenu(menuRect, "smoothingMode",
7025                                (int)ControllerModulationSource::SmoothingMode::LEGACY,
7026                                [this](auto md) { this->resetSmoothing(md); });
7027     midiSubMenu->addEntry(smen, Surge::UI::toOSCaseForMenu("Controller Smoothing"));
7028     smen->forget();
7029 
7030     auto mmom = makeMonoModeOptionsMenu(menuRect, true);
7031     midiSubMenu->addEntry(mmom, Surge::UI::toOSCaseForMenu("Sustain Pedal In Mono Mode"));
7032     mmom->forget();
7033     midiSubMenu->addSeparator();
7034 
7035     addCallbackMenu(midiSubMenu, Surge::UI::toOSCaseForMenu("Save MIDI Mapping As..."),
7036                     [this, menuRect]() {
7037                         this->scannedForMidiPresets = false; // force a rescan
7038                         char msn[256];
7039                         msn[0] = 0;
7040                         promptForMiniEdit(msn, "MIDI Mapping Name", "Save MIDI Mapping",
7041                                           menuRect.getTopLeft(), [this](const std::string &s) {
7042                                               this->synth->storage.storeMidiMappingToName(s);
7043                                           });
7044                     });
7045 
7046     auto menuItem = addCallbackMenu(
7047         midiSubMenu, Surge::UI::toOSCaseForMenu("Set Current MIDI Mapping as Default"),
7048         [this]() { this->synth->storage.write_midi_controllers_to_user_default(); });
7049 
7050     addCallbackMenu(
7051         midiSubMenu, Surge::UI::toOSCaseForMenu("Clear Current MIDI Mapping"), [this]() {
7052             int n = n_global_params + n_scene_params;
7053 
7054             for (int i = 0; i < n; i++)
7055             {
7056                 this->synth->storage.getPatch().param_ptr[i]->midictrl = -1;
7057                 if (i > n_global_params)
7058                     this->synth->storage.getPatch().param_ptr[i + n_scene_params]->midictrl = -1;
7059             }
7060         });
7061 
7062     midiSubMenu->addSeparator();
7063 
7064     addCallbackMenu(midiSubMenu, Surge::UI::toOSCaseForMenu("Show Current MIDI Mapping..."),
7065                     [this]() { Surge::UserInteractions::showHTML(this->midiMappingToHtml()); });
7066 
7067     if (!scannedForMidiPresets)
7068     {
7069         scannedForMidiPresets = true;
7070         synth->storage.rescanUserMidiMappings();
7071     }
7072 
7073     bool gotOne = false;
7074     for (const auto &p : synth->storage.userMidiMappingsXMLByName)
7075     {
7076         if (!gotOne)
7077         {
7078             gotOne = true;
7079             midiSubMenu->addSeparator();
7080         }
7081         addCallbackMenu(midiSubMenu, p.first,
7082                         [this, p] { this->synth->storage.loadMidiMappingByName(p.first); });
7083     }
7084 
7085     return midiSubMenu;
7086 }
7087 
reloadFromSkin()7088 void SurgeGUIEditor::reloadFromSkin()
7089 {
7090     if (!frame || !bitmapStore.get())
7091         return;
7092 
7093     float dbs = Surge::GUI::getDisplayBackingScaleFactor(getFrame());
7094     bitmapStore->setPhysicalZoomFactor(getZoomFactor() * dbs);
7095 
7096     auto bg = currentSkin->customBackgroundImage();
7097 
7098     if (bg != "")
7099     {
7100         CScalableBitmap *cbm = bitmapStore->getBitmapByStringID(bg);
7101         if (cbm)
7102         {
7103             cbm->setExtraScaleFactor(getZoomFactor());
7104             frame->setBackground(cbm);
7105         }
7106     }
7107     else
7108     {
7109         CScalableBitmap *cbm = bitmapStore->getBitmap(IDB_MAIN_BG);
7110         cbm->setExtraScaleFactor(getZoomFactor());
7111         frame->setBackground(cbm);
7112     }
7113 
7114     auto c = currentSkin->getColor(Colors::Dialog::Entry::Focus);
7115     frame->setFocusColor(c);
7116 
7117     wsx = currentSkin->getWindowSizeX();
7118     wsy = currentSkin->getWindowSizeY();
7119 
7120     float sf = 1;
7121 #if TARGET_VST2
7122     sf = getZoomFactor() / 100.0;
7123 #endif
7124     frame->setSize(wsx * sf, wsy * sf);
7125 
7126 #if TARGET_VST3
7127     float uzf = getZoomFactor();
7128 
7129     if (currentSkin->hasFixedZooms())
7130     {
7131         for (auto z : currentSkin->getFixedZooms())
7132         {
7133             if (z <= zoomFactor)
7134             {
7135                 uzf = z;
7136             }
7137         }
7138     }
7139 
7140     resizeToOnIdle = VSTGUI::CPoint(wsx * uzf / 100.0, wsy * uzf / 100.0);
7141 
7142     sf = uzf / 100.0;
7143 #endif
7144 
7145     rect.right = wsx * sf;
7146     rect.bottom = wsy * sf;
7147 
7148 #if TARGET_JUCE_UI
7149     setZoomFactor(getZoomFactor(), true);
7150 #else
7151     setZoomFactor(getZoomFactor());
7152 #endif
7153     clearOffscreenCachesAtZero = 1;
7154 
7155     // update MSEG editor if opened
7156     if (isAnyOverlayPresent(MSEG_EDITOR))
7157     {
7158         showMSEGEditor();
7159     }
7160 
7161     // update Store Patch dialog if opened
7162     if (isAnyOverlayPresent(STORE_PATCH))
7163     {
7164         auto pname = patchName->getText();
7165         auto pcat = patchCategory->getText();
7166         auto pauth = patchCreator->getText();
7167         auto pcom = patchComment->getText();
7168 
7169         showStorePatchDialog();
7170 
7171         patchName->setText(pname);
7172         patchCategory->setText(pcat);
7173         patchCreator->setText(pauth);
7174         patchComment->setText(pcom);
7175     }
7176 }
7177 
makeDevMenu(VSTGUI::CRect & menuRect)7178 VSTGUI::COptionMenu *SurgeGUIEditor::makeDevMenu(VSTGUI::CRect &menuRect)
7179 {
7180     int tid = 0;
7181 
7182     COptionMenu *devSubMenu = new COptionMenu(menuRect, 0, 0, 0, 0,
7183                                               VSTGUI::COptionMenu::kNoDrawStyle |
7184                                                   VSTGUI::COptionMenu::kMultipleCheckStyle);
7185 
7186 #if WINDOWS
7187     VSTGUI::CCommandMenuItem *conItem = nullptr;
7188 
7189     static bool consoleState;
7190 
7191     conItem = addCallbackMenu(devSubMenu, Surge::UI::toOSCaseForMenu("Show Debug Console..."),
7192                               []() { consoleState = Surge::Debug::toggleConsole(); });
7193     conItem->setChecked(consoleState);
7194     tid++;
7195 #endif
7196 
7197 #ifdef INSTRUMENT_UI
7198     addCallbackMenu(devSubMenu, Surge::UI::toOSCaseForMenu("Show UI Instrumentation..."),
7199                     []() { Surge::Debug::report(); });
7200     tid++;
7201 #endif
7202 
7203     return devSubMenu;
7204 }
7205 
findLargestFittingZoomBetween(int zoomLow,int zoomHigh,int zoomQuanta,int percentageOfScreenAvailable,float baseW,float baseH)7206 int SurgeGUIEditor::findLargestFittingZoomBetween(
7207     int zoomLow,                     // bottom of range
7208     int zoomHigh,                    // top of range
7209     int zoomQuanta,                  // step size
7210     int percentageOfScreenAvailable, // How much to shrink actual screen
7211     float baseW, float baseH)
7212 {
7213     // Here is a very crude implementation
7214     int result = zoomHigh;
7215     CRect screenDim = Surge::GUI::getScreenDimensions(getFrame());
7216     float sx = screenDim.getWidth() * percentageOfScreenAvailable / 100.0;
7217     float sy = screenDim.getHeight() * percentageOfScreenAvailable / 100.0;
7218 
7219     while (result > zoomLow)
7220     {
7221         if (result * baseW / 100.0 <= sx && result * baseH / 100.0 <= sy)
7222             break;
7223         result -= zoomQuanta;
7224     }
7225     if (result < zoomLow)
7226         result = zoomLow;
7227 
7228     return result;
7229 }
7230 
addCallbackMenu(VSTGUI::COptionMenu * toThis,std::string label,std::function<void ()> op)7231 VSTGUI::CCommandMenuItem *SurgeGUIEditor::addCallbackMenu(VSTGUI::COptionMenu *toThis,
7232                                                           std::string label,
7233                                                           std::function<void()> op)
7234 {
7235     CCommandMenuItem *menu = new CCommandMenuItem(CCommandMenuItem::Desc(label.c_str()));
7236     menu->setActions([op](CCommandMenuItem *m) { op(); });
7237     toThis->addEntry(menu);
7238     return menu;
7239 }
7240 
7241 #if TARGET_VST3
initialZoom()7242 bool SurgeGUIEditor::initialZoom()
7243 {
7244     if (initialZoomFactor != 100)
7245     {
7246         // Daw does not necessarily have any idea what size to draw on init. We need to tell it.
7247         setZoomFactor(initialZoomFactor);
7248         initialZoomFactor = 100;
7249 
7250         // Important: this is the one and only window resize operation allowed inside onSize.
7251         // Infinite recursion warning!
7252         zoom_callback(this, true);
7253         return true;
7254     }
7255     return false;
7256 }
7257 
resizeFromIdleSentinel()7258 void SurgeGUIEditor::resizeFromIdleSentinel()
7259 {
7260     if (resizeToOnIdle.x > 10 && resizeToOnIdle.y > 10)
7261     {
7262         Steinberg::IPlugFrame *ipf = getIPlugFrame();
7263         if (ipf)
7264         {
7265             Steinberg::ViewRect vr(0, 0, resizeToOnIdle.x, resizeToOnIdle.y);
7266             Steinberg::tresult res = ipf->resizeView(this, &vr);
7267             resizeToOnIdle = VSTGUI::CPoint(-1, -1);
7268         }
7269     }
7270 }
7271 
onSize(Steinberg::ViewRect * newSize)7272 Steinberg::tresult PLUGIN_API SurgeGUIEditor::onSize(Steinberg::ViewRect *newSize)
7273 {
7274     if (!initialZoom())
7275     {
7276         float width = newSize->getWidth();
7277         float height = newSize->getHeight();
7278 
7279         // resolve current zoomFactor
7280         float izfx = ceil(width / getWindowSizeX() * 100.0);
7281         float izfy = ceil(height / getWindowSizeY() * 100.0);
7282         float currentZoomFactor = std::min(izfx, izfy);
7283         currentZoomFactor = std::max(currentZoomFactor, 1.0f * minimumZoom);
7284 
7285         // Don't allow me to set a zoom which will pop a dialog from this drag event. See #1212
7286         float correctedZoomFactor;
7287 
7288         if (!doesZoomFitToScreen(currentZoomFactor, correctedZoomFactor))
7289         {
7290             currentZoomFactor = correctedZoomFactor;
7291         }
7292 
7293         // If the resolved currentZoomFactor changes size by more than a pixel, store new size
7294         auto zfdx = std::fabs((currentZoomFactor - zoomFactor) * getWindowSizeX()) / 100.0;
7295         auto zfdy = std::fabs((currentZoomFactor - zoomFactor) * getWindowSizeY()) / 100.0;
7296         bool isFixed = currentSkin->hasFixedZooms();
7297         bool windowDragResize = std::max(zfdx, zfdy) > 1;
7298         bool allowDragResize =
7299             Surge::Storage::getUserDefaultValue(&(this->synth->storage), "dragResizeVST3", true);
7300 
7301         if (allowDragResize && windowDragResize && !isFixed)
7302         {
7303             setZoomFactor(currentZoomFactor);
7304         }
7305     }
7306 
7307     return Steinberg::Vst::VSTGUIEditor::onSize(newSize);
7308 }
7309 
checkSizeConstraint(Steinberg::ViewRect * newSize)7310 Steinberg::tresult PLUGIN_API SurgeGUIEditor::checkSizeConstraint(Steinberg::ViewRect *newSize)
7311 {
7312     // we want cratio == tration by adjusting height so
7313     // WSX / WSY = gW / gH
7314     // gH = gW * WSY / WSX
7315     if (synth->hostProgram.find("Fruit") != std::string::npos) // see #2466
7316     {
7317         return EditorType::checkSizeConstraint(newSize);
7318     }
7319     else
7320     {
7321         float tratio = 1.0 * getWindowSizeX() / getWindowSizeY();
7322         float cratio = 1.0 * newSize->getWidth() / newSize->getHeight();
7323         if (cratio < tratio)
7324         {
7325             float newHeight = newSize->getWidth() / tratio;
7326             newSize->bottom = newSize->top + std::ceil(newHeight);
7327         }
7328         else
7329         {
7330             float newWidth = newSize->getHeight() * tratio;
7331             newSize->right = newSize->left + std::ceil(newWidth);
7332         }
7333 
7334         return Steinberg::kResultTrue;
7335     }
7336 }
7337 
7338 #endif
7339 
forceautomationchangefor(Parameter * p)7340 void SurgeGUIEditor::forceautomationchangefor(Parameter *p)
7341 {
7342 #if TARGET_LV2
7343     // revisit this for non-LV2 outside 1.6.6
7344     synth->sendParameterAutomation(synth->idForParameter(p),
7345                                    synth->getParameter01(synth->idForParameter(p)));
7346 #endif
7347 }
7348 //------------------------------------------------------------------------------------------------
7349 
promptForUserValueEntry(Parameter * p,CControl * c,int ms)7350 void SurgeGUIEditor::promptForUserValueEntry(Parameter *p, CControl *c, int ms)
7351 {
7352     if (typeinDialog)
7353     {
7354         typeinDialog->setVisible(false);
7355         removeFromFrame.push_back(typeinDialog);
7356         typeinDialog = nullptr;
7357         typeinResetCounter = -1;
7358     }
7359 
7360     if (p)
7361     {
7362         typeinMode = Param;
7363     }
7364     else
7365     {
7366         typeinMode = Control;
7367     }
7368 
7369     bool ismod = p && ms > 0;
7370     int boxht = 56;
7371     auto cp = c->getViewSize();
7372 
7373     if (ismod)
7374         boxht += 22;
7375 
7376     CRect typeinSize(cp.left, cp.top - boxht, cp.left + 120, cp.top - boxht + boxht);
7377 
7378     if (cp.top - boxht < 0)
7379     {
7380         typeinSize =
7381             CRect(cp.left, cp.top + c->getHeight(), cp.left + 120, cp.top + c->getHeight() + boxht);
7382     }
7383 
7384     typeinModSource = ms;
7385     typeinEditControl = c;
7386     typeinResetCounter = -1;
7387 
7388     typeinDialog = new CViewContainer(typeinSize);
7389     typeinDialog->setBackgroundColor(currentSkin->getColor(Colors::Dialog::Border));
7390     typeinDialog->setVisible(false);
7391     frame->addView(typeinDialog);
7392 
7393     CRect innerSize(0, 0, typeinSize.getWidth(), typeinSize.getHeight());
7394     innerSize.inset(1, 1);
7395     auto inner = new CViewContainer(innerSize);
7396     CColor bggr = currentSkin->getColor(Colors::Dialog::Background);
7397     inner->setBackgroundColor(bggr);
7398     typeinDialog->addView(inner);
7399 
7400     std::string lab = "";
7401     if (p)
7402     {
7403         if (p->ctrlgroup == cg_LFO)
7404         {
7405             char pname[1024];
7406             p->create_fullname(p->get_name(), pname, p->ctrlgroup, p->ctrlgroup_entry,
7407                                modulatorName(p->ctrlgroup_entry, true).c_str());
7408             lab = pname;
7409         }
7410         else
7411         {
7412             lab = p->get_full_name();
7413         }
7414     }
7415     else
7416     {
7417         lab = modulatorName(ms, false);
7418     }
7419 
7420     typeinLabel = new CTextLabel(CRect(2, 2, 114, 14), lab.c_str());
7421     typeinLabel->setFontColor(currentSkin->getColor(Colors::Slider::Label::Dark));
7422     typeinLabel->setTransparency(true);
7423     typeinLabel->setFont(displayFont);
7424     inner->addView(typeinLabel);
7425 
7426     char txt[256];
7427     char ptext[1024], ptext2[1024];
7428     ptext2[0] = 0;
7429     if (p)
7430     {
7431         if (ismod)
7432         {
7433             char txt2[256];
7434             p->get_display_of_modulation_depth(txt, synth->getModDepth(p->id, (modsources)ms),
7435                                                synth->isBipolarModulation((modsources)ms),
7436                                                Parameter::TypeIn);
7437             p->get_display(txt2);
7438             sprintf(ptext, "mod: %s", txt);
7439             sprintf(ptext2, "current: %s", txt2);
7440         }
7441         else
7442         {
7443             p->get_display(txt);
7444             sprintf(ptext, "current: %s", txt);
7445         }
7446     }
7447     else
7448     {
7449         int detailedMode = Surge::Storage::getUserDefaultValue(&(this->synth->storage),
7450                                                                "highPrecisionReadouts", 0);
7451         auto cms = ((ControllerModulationSource *)synth->storage.getPatch()
7452                         .scene[current_scene]
7453                         .modsources[ms]);
7454         sprintf(txt, "%.*f %%", (detailedMode ? 6 : 2), 100.0 * cms->get_output());
7455         sprintf(ptext, "current: %s", txt);
7456     }
7457 
7458     if (ismod)
7459     {
7460         std::string mls = std::string("by ") + (char *)modulatorName(ms, true).c_str();
7461         auto ml = new CTextLabel(CRect(2, 10, 114, 27), mls.c_str());
7462         ml->setFontColor(currentSkin->getColor(Colors::Slider::Label::Dark));
7463         ml->setTransparency(true);
7464         ml->setFont(displayFont);
7465         inner->addView(ml);
7466     }
7467 
7468     typeinPriorValueLabel = new CTextLabel(CRect(2, 29 - (ismod ? 0 : 23), 116, 36 + ismod), ptext);
7469     typeinPriorValueLabel->setFontColor(currentSkin->getColor(Colors::Slider::Label::Dark));
7470     typeinPriorValueLabel->setTransparency(true);
7471     typeinPriorValueLabel->setFont(displayFont);
7472     inner->addView(typeinPriorValueLabel);
7473 
7474     if (ismod)
7475     {
7476         auto sl = new CTextLabel(CRect(2, 29 + 9, 116, 36 + 13), ptext2);
7477         sl->setFontColor(currentSkin->getColor(Colors::Slider::Label::Dark));
7478         sl->setTransparency(true);
7479         sl->setFont(displayFont);
7480         inner->addView(sl);
7481     }
7482 
7483     typeinValue = new CTextEdit(CRect(4, 31 + (ismod ? 22 : 0), 114, 50 + (ismod ? 22 : 0)), this,
7484                                 tag_value_typein, txt);
7485     typeinValue->setBackColor(currentSkin->getColor(Colors::Dialog::Entry::Background));
7486     typeinValue->setFontColor(currentSkin->getColor(Colors::Dialog::Entry::Text));
7487 
7488     // fix the text selection rectangle background overhanging the borders on Windows
7489 #if WINDOWS
7490     typeinValue->setTextInset(CPoint(3, 0));
7491 #endif
7492 
7493     if (p)
7494     {
7495         if (!p->can_setvalue_from_string())
7496         {
7497             typeinValue->setFontColor(VSTGUI::kRedCColor);
7498             typeinValue->setText("Not available");
7499         }
7500     }
7501 
7502     inner->addView(typeinValue);
7503     typeinEditTarget = p;
7504     typeinDialog->setVisible(true);
7505     typeinValue->takeFocus();
7506 }
7507 
modulatorName(int i,bool button)7508 std::string SurgeGUIEditor::modulatorName(int i, bool button)
7509 {
7510     if ((i >= ms_lfo1 && i <= ms_slfo6))
7511     {
7512         int idx = i - ms_lfo1;
7513         bool isS = idx >= 6;
7514         int fnum = idx % 6;
7515         auto *lfodata = &(synth->storage.getPatch().scene[current_scene].lfo[i - ms_lfo1]);
7516 
7517         if (lfodata->shape.val.i == lt_envelope)
7518         {
7519             char txt[64];
7520             if (button)
7521                 sprintf(txt, "%sENV %d", (isS ? "S-" : ""), fnum + 1);
7522             else
7523                 sprintf(txt, "%s Envelope %d", (isS ? "Scene" : "Voice"), fnum + 1);
7524             return std::string(txt);
7525         }
7526         else if (lfodata->shape.val.i == lt_stepseq)
7527         {
7528             char txt[64];
7529             if (button)
7530                 sprintf(txt, "%sSEQ %d", (isS ? "S-" : ""), fnum + 1);
7531             else
7532                 sprintf(txt, "%s Step Sequencer %d", (isS ? "Scene" : "Voice"), fnum + 1);
7533             return std::string(txt);
7534         }
7535         else if (lfodata->shape.val.i == lt_mseg)
7536         {
7537             char txt[64];
7538             if (button)
7539                 sprintf(txt, "%sMSEG %d", (isS ? "S-" : ""), fnum + 1);
7540             else
7541                 sprintf(txt, "%s MSEG %d", (isS ? "Scene" : "Voice"), fnum + 1);
7542             return std::string(txt);
7543         }
7544         else if (lfodata->shape.val.i == lt_function)
7545         {
7546             char txt[64];
7547 
7548             // TODO FIXME: When function LFO type is added, uncomment the second sprintf and remove
7549             // the first one!
7550             if (button)
7551                 sprintf(txt, "%sENV %d", (isS ? "S-" : ""), fnum + 1);
7552             // sprintf( txt, "%sFUN %d", (isS ? "S-" : "" ), fnum + 1 );
7553             else
7554                 sprintf(txt, "%s Envelope %d", (isS ? "Scene" : "Voice"), fnum + 1);
7555             // sprintf( txt, "%s Function %d", (isS ? "Scene" : "Voice" ), fnum + 1 );
7556             return std::string(txt);
7557         }
7558     }
7559 
7560     if (i >= ms_ctrl1 && i <= ms_ctrl8)
7561     {
7562         std::string ccl =
7563             std::string(synth->storage.getPatch().CustomControllerLabel[i - ms_ctrl1]);
7564         if (ccl == "-")
7565         {
7566             return std::string(modsource_names[i]);
7567         }
7568         else
7569         {
7570             return ccl + " (" + modsource_names[i] + ")";
7571         }
7572     }
7573     if (button)
7574         return std::string(modsource_names_button[i]);
7575     else
7576         return std::string(modsource_names[i]);
7577 }
7578 
7579 #if TARGET_VST3
addVst3MenuForParams(VSTGUI::COptionMenu * contextMenu,const SurgeSynthesizer::ID & pid,int & eid)7580 Steinberg::Vst::IContextMenu *SurgeGUIEditor::addVst3MenuForParams(VSTGUI::COptionMenu *contextMenu,
7581                                                                    const SurgeSynthesizer::ID &pid,
7582                                                                    int &eid)
7583 {
7584     CRect menuRect;
7585     Steinberg::Vst::IComponentHandler *componentHandler = getController()->getComponentHandler();
7586     Steinberg::FUnknownPtr<Steinberg::Vst::IComponentHandler3> componentHandler3(componentHandler);
7587     Steinberg::Vst::IContextMenu *hostMenu = nullptr;
7588     if (componentHandler3)
7589     {
7590         std::stack<COptionMenu *> menuStack;
7591         menuStack.push(contextMenu);
7592         std::stack<int> eidStack;
7593         eidStack.push(eid);
7594 
7595         Steinberg::Vst::ParamID param = pid.getDawSideId();
7596         hostMenu = componentHandler3->createContextMenu(this, &param);
7597 
7598         int N = hostMenu ? hostMenu->getItemCount() : 0;
7599         if (N > 0)
7600         {
7601             contextMenu->addSeparator();
7602             eid++;
7603         }
7604 
7605         std::deque<COptionMenu *> parentMenus;
7606         for (int i = 0; i < N; i++)
7607         {
7608             Steinberg::Vst::IContextMenu::Item item = {0};
7609             Steinberg::Vst::IContextMenuTarget *target = {0};
7610 
7611             hostMenu->getItem(i, item, &target);
7612 
7613             // char nm[1024];
7614             // Steinberg::UString128(item.name, 128).toAscii(nm, 1024);
7615 #if WINDOWS
7616             // https://stackoverflow.com/questions/32055357/visual-studio-c-2015-stdcodecvt-with-char16-t-or-char32-t
7617             std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> conversion;
7618             std::string nm = conversion.to_bytes((wchar_t *)(item.name));
7619 #else
7620             std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> conversion;
7621             std::string nm = conversion.to_bytes((char16_t *)(item.name));
7622 #endif
7623 
7624             if (nm[0] ==
7625                 '-') // FL sends us this as a separator with no VST indication so just strip the '-'
7626             {
7627                 int pos = 1;
7628                 while (nm[pos] == ' ' && nm[pos] != 0)
7629                     pos++;
7630                 nm = nm.substr(pos);
7631             }
7632 
7633             auto itag = item.tag;
7634             /*
7635             ** Leave this here so we can debug if another vst3 problem comes up
7636             std::cout << nm << " FL=" << item.flags << " jGS=" <<
7637             Steinberg::Vst::IContextMenuItem::kIsGroupStart
7638             << " and=" << ( item.flags & Steinberg::Vst::IContextMenuItem::kIsGroupStart )
7639             << " IGS="
7640             << ( ( item.flags & Steinberg::Vst::IContextMenuItem::kIsGroupStart ) ==
7641             Steinberg::Vst::IContextMenuItem::kIsGroupStart ) << " IGE="
7642             << ( ( item.flags & Steinberg::Vst::IContextMenuItem::kIsGroupEnd ) ==
7643             Steinberg::Vst::IContextMenuItem::kIsGroupEnd ) << " "
7644             << std::endl;
7645 
7646             if( item.flags != 0 )
7647                printf( "FLAG %d IGS %d IGE %d SEP %d\n",
7648                       item.flags,
7649                       item.flags & Steinberg::Vst::IContextMenuItem::kIsGroupStart,
7650                       item.flags & Steinberg::Vst::IContextMenuItem::kIsGroupEnd,
7651                       item.flags & Steinberg::Vst::IContextMenuItem::kIsSeparator
7652                       );
7653                       */
7654             if ((item.flags & Steinberg::Vst::IContextMenuItem::kIsGroupStart) ==
7655                 Steinberg::Vst::IContextMenuItem::kIsGroupStart)
7656             {
7657                 COptionMenu *subMenu = new COptionMenu(
7658                     menuRect, 0, 0, 0, 0,
7659                     VSTGUI::COptionMenu::kNoDrawStyle | VSTGUI::COptionMenu::kMultipleCheckStyle);
7660                 menuStack.top()->addEntry(subMenu, nm.c_str());
7661                 menuStack.push(subMenu);
7662                 subMenu->forget();
7663                 eidStack.push(0);
7664 
7665                 /*
7666                   VSTGUI doesn't seem to allow a disabled or checked grouping menu.
7667                   if( item.flags & Steinberg::Vst::IContextMenuItem::kIsDisabled )
7668                   {
7669                   subMenu->setEnabled(false);
7670                   }
7671                   if( item.flags & Steinberg::Vst::IContextMenuItem::kIsChecked )
7672                   {
7673                   subMenu->setChecked(true);
7674                   }
7675                 */
7676             }
7677             else if ((item.flags & Steinberg::Vst::IContextMenuItem::kIsGroupEnd) ==
7678                      Steinberg::Vst::IContextMenuItem::kIsGroupEnd)
7679             {
7680                 menuStack.pop();
7681                 eidStack.pop();
7682             }
7683             else if (item.flags & Steinberg::Vst::IContextMenuItem::kIsSeparator)
7684             // separator not group end. Thanks for the insane definition of these constants VST3!
7685             // (See #3090)
7686             {
7687                 menuStack.top()->addSeparator();
7688             }
7689             else
7690             {
7691                 RememberForgetGuard<Steinberg::Vst::IContextMenuTarget> tg(target);
7692                 RememberForgetGuard<Steinberg::Vst::IContextMenu> hm(hostMenu);
7693 
7694                 auto menu = addCallbackMenu(
7695                     menuStack.top(), nm, [this, hm, tg, itag]() { tg.t->executeMenuItem(itag); });
7696                 eidStack.top()++;
7697                 if (item.flags & Steinberg::Vst::IContextMenuItem::kIsDisabled)
7698                 {
7699                     menu->setEnabled(false);
7700                 }
7701                 if (item.flags & Steinberg::Vst::IContextMenuItem::kIsChecked)
7702                 {
7703                     menu->setChecked(true);
7704                 }
7705             }
7706             // hostMenu->addItem(item, &target);
7707         }
7708         eid = eidStack.top();
7709     }
7710     return hostMenu;
7711 }
7712 #endif
7713 
helpURLFor(Parameter * p)7714 std::string SurgeGUIEditor::helpURLFor(Parameter *p)
7715 {
7716     auto storage = &(synth->storage);
7717 #if 0 // useful debug
7718    static bool once = false;
7719    if( ! once )
7720    {
7721       once = true;
7722       for( auto hp : storage->helpURL_paramidentifier )
7723       {
7724          auto k = hp.first;
7725          bool found = false;
7726          for (auto iter = synth->storage.getPatch().param_ptr.begin();
7727               iter != synth->storage.getPatch().param_ptr.end(); iter++)
7728          {
7729             Parameter* q = *iter;
7730             if( ! q ) break;
7731             if( q->ui_identifier == k )
7732             {
7733                found = true;
7734                break;
7735             }
7736          }
7737          if( ! found )
7738             std::cout << "UNFOUND : " << k << std::endl;
7739       }
7740    }
7741 #endif
7742     std::string id = p->ui_identifier;
7743     int type = -1;
7744     if (p->ctrlgroup == cg_OSC)
7745     {
7746         type = storage->getPatch().scene[current_scene].osc[current_osc[current_scene]].type.val.i;
7747     }
7748     if (p->ctrlgroup == cg_FX)
7749     {
7750         type = storage->getPatch().fx[current_fx].type.val.i;
7751     }
7752     if (type >= 0)
7753     {
7754         auto key = std::make_pair(id, type);
7755         if (storage->helpURL_paramidentifier_typespecialized.find(key) !=
7756             storage->helpURL_paramidentifier_typespecialized.end())
7757         {
7758             auto r = storage->helpURL_paramidentifier_typespecialized[key];
7759             if (r != "")
7760                 return r;
7761         }
7762     }
7763     if (storage->helpURL_paramidentifier.find(id) != storage->helpURL_paramidentifier.end())
7764     {
7765         auto r = storage->helpURL_paramidentifier[id];
7766         if (r != "")
7767             return r;
7768     }
7769     if (storage->helpURL_controlgroup.find(p->ctrlgroup) != storage->helpURL_controlgroup.end())
7770     {
7771         auto r = storage->helpURL_controlgroup[p->ctrlgroup];
7772         if (r != "")
7773             return r;
7774     }
7775     return "";
7776 }
7777 
helpURLForSpecial(std::string key)7778 std::string SurgeGUIEditor::helpURLForSpecial(std::string key)
7779 {
7780     auto storage = &(synth->storage);
7781     return helpURLForSpecial(storage, key);
7782 }
7783 
helpURLForSpecial(SurgeStorage * storage,std::string key)7784 std::string SurgeGUIEditor::helpURLForSpecial(SurgeStorage *storage, std::string key)
7785 {
7786     if (storage->helpURL_specials.find(key) != storage->helpURL_specials.end())
7787     {
7788         auto r = storage->helpURL_specials[key];
7789         if (r != "")
7790             return r;
7791     }
7792     return "";
7793 }
fullyResolvedHelpURL(std::string helpurl)7794 std::string SurgeGUIEditor::fullyResolvedHelpURL(std::string helpurl)
7795 {
7796     std::string lurl = helpurl;
7797     if (helpurl[0] == '#')
7798     {
7799         lurl = "https://surge-synthesizer.github.io/manual/" + helpurl;
7800     }
7801     return lurl;
7802 }
7803 
setupSkinFromEntry(const Surge::UI::SkinDB::Entry & entry)7804 void SurgeGUIEditor::setupSkinFromEntry(const Surge::UI::SkinDB::Entry &entry)
7805 {
7806     auto &db = Surge::UI::SkinDB::get();
7807     auto s = db.getSkin(entry);
7808     this->currentSkin = s;
7809     this->bitmapStore.reset(new SurgeBitmaps());
7810     this->bitmapStore->setupBitmapsForFrame(frame);
7811     if (!this->currentSkin->reloadSkin(this->bitmapStore))
7812     {
7813         std::ostringstream oss;
7814         oss << "Unable to load " << entry.root << entry.name
7815             << " skin! Reverting the skin to Surge Classic.\n\nSkin Error:\n"
7816             << db.getAndResetErrorString();
7817 
7818         auto msg = std::string(oss.str());
7819         this->currentSkin = db.defaultSkin(&(this->synth->storage));
7820         this->currentSkin->reloadSkin(this->bitmapStore);
7821         Surge::UserInteractions::promptError(msg, "Skin Loading Error");
7822     }
7823     reloadFromSkin();
7824 }
7825 
sliderHoverStart(int tag)7826 void SurgeGUIEditor::sliderHoverStart(int tag)
7827 {
7828     int ptag = tag - start_paramtags;
7829     for (int k = 1; k < n_modsources; k++)
7830     {
7831         modsources ms = (modsources)k;
7832         if (synth->isActiveModulation(ptag, ms))
7833         {
7834             auto gms = dynamic_cast<CModulationSourceButton *>(gui_modsrc[k]);
7835             if (gms)
7836             {
7837                 gms->setSecondaryHover(true);
7838             }
7839         }
7840     };
7841 }
sliderHoverEnd(int tag)7842 void SurgeGUIEditor::sliderHoverEnd(int tag)
7843 {
7844     for (int k = 1; k < n_modsources; k++)
7845     {
7846         auto gms = dynamic_cast<CModulationSourceButton *>(gui_modsrc[k]);
7847         if (gms)
7848         {
7849             gms->setSecondaryHover(false);
7850         }
7851     }
7852 }
7853 
dismissEditorOfType(OverlayTags ofType)7854 void SurgeGUIEditor::dismissEditorOfType(OverlayTags ofType)
7855 {
7856     if (editorOverlay.size() == 0)
7857         return;
7858 
7859     auto newO = editorOverlay;
7860     newO.clear();
7861     for (auto el : editorOverlay)
7862     {
7863         if (el.first == ofType)
7864         {
7865             el.second->setVisible(false);
7866             auto f = editorOverlayOnClose[el.second];
7867             f();
7868 
7869             editorOverlayOnClose.erase(el.second);
7870             editorOverlayContentsWeakReference.erase(el.second);
7871 
7872             removeFromFrame.push_back(el.second);
7873         }
7874         else
7875         {
7876             newO.push_back(el);
7877         }
7878     }
7879     editorOverlay = newO;
7880 }
7881 
addEditorOverlay(VSTGUI::CView * c,std::string editorTitle,OverlayTags editorTag,const VSTGUI::CPoint & topLeft,bool modalOverlay,bool hasCloseButton,std::function<void ()> onClose)7882 void SurgeGUIEditor::addEditorOverlay(VSTGUI::CView *c, std::string editorTitle,
7883                                       OverlayTags editorTag, const VSTGUI::CPoint &topLeft,
7884                                       bool modalOverlay, bool hasCloseButton,
7885                                       std::function<void()> onClose)
7886 {
7887     dismissEditorOfType(editorTag);
7888 
7889     const int header = 18;
7890     const int buttonwidth = 18;
7891 
7892     if (!c)
7893         return;
7894 
7895     auto vs = c->getViewSize();
7896 
7897     // add a solid outline thing which is bigger than the control with the title to that
7898     auto containerSize = vs;
7899     containerSize.top -= header;
7900 
7901     if (!modalOverlay)
7902         containerSize.offset(-containerSize.left + topLeft.x, -containerSize.top + topLeft.y);
7903 
7904     auto fs = CRect(0, 0, getWindowSizeX(), getWindowSizeY());
7905 
7906     if (!modalOverlay)
7907         fs = containerSize;
7908 
7909     // add a screen size transparent thing into the editorOverlay
7910     auto editorOverlayC = new CViewContainer(fs);
7911     editorOverlayC->setBackgroundColor(currentSkin->getColor(Colors::Overlay::Background));
7912     editorOverlayC->setVisible(true);
7913     frame->addView(editorOverlayC);
7914 
7915     if (modalOverlay)
7916         containerSize = containerSize.centerInside(fs);
7917     else
7918         containerSize.moveTo(CPoint(0, 0));
7919 
7920     auto headerFont = Surge::GUI::getLatoAtSize(9, kBoldFace);
7921     auto btnFont = Surge::GUI::getLatoAtSize(8);
7922 
7923     auto outerc = new CViewContainer(containerSize);
7924     outerc->setBackgroundColor(currentSkin->getColor(Colors::Dialog::Border));
7925     editorOverlayC->addView(outerc);
7926 
7927     auto csz = containerSize;
7928     csz.bottom = csz.top + header;
7929     auto innerc = new CViewContainer(csz);
7930     innerc->setBackgroundColor(currentSkin->getColor(Colors::Dialog::Titlebar::Background));
7931     editorOverlayC->addView(innerc);
7932 
7933     auto tl = new CTextLabel(csz, editorTitle.c_str());
7934     tl->setBackColor(currentSkin->getColor(Colors::Dialog::Titlebar::Background));
7935     tl->setFrameColor(currentSkin->getColor(Colors::Dialog::Titlebar::Background));
7936     tl->setFontColor(currentSkin->getColor(Colors::Dialog::Titlebar::Text));
7937     tl->setFont(headerFont);
7938     innerc->addView(tl);
7939 
7940     auto iconrect = CRect(CPoint(3, 2), CPoint(15, 15));
7941     auto icon = new CViewContainer(iconrect);
7942     icon->setBackground(bitmapStore->getBitmap(IDB_SURGE_ICON));
7943     icon->setVisible(true);
7944     innerc->addView(icon);
7945 
7946     if (hasCloseButton)
7947     {
7948         auto btnbg = currentSkin->getColor(Colors::Dialog::Button::Background);
7949         auto btnborder = currentSkin->getColor(Colors::Dialog::Button::Border);
7950         auto btntext = currentSkin->getColor(Colors::Dialog::Button::Text);
7951 
7952         auto hovbtnbg = currentSkin->getColor(Colors::Dialog::Button::BackgroundHover);
7953         auto hovbtnborder = currentSkin->getColor(Colors::Dialog::Button::BorderHover);
7954         auto hovbtntext = currentSkin->getColor(Colors::Dialog::Button::TextHover);
7955 
7956         auto pressbtnbg = currentSkin->getColor(Colors::Dialog::Button::BackgroundPressed);
7957         auto pressbtnborder = currentSkin->getColor(Colors::Dialog::Button::BorderPressed);
7958         auto pressbtntext = currentSkin->getColor(Colors::Dialog::Button::TextPressed);
7959 
7960         VSTGUI::CGradient::ColorStopMap csm;
7961         VSTGUI::CGradient *cg = VSTGUI::CGradient::create(csm);
7962         cg->addColorStop(0, btnbg);
7963 
7964         VSTGUI::CGradient::ColorStopMap hovcsm;
7965         VSTGUI::CGradient *hovcg = VSTGUI::CGradient::create(hovcsm);
7966         hovcg->addColorStop(0, hovbtnbg);
7967 
7968         VSTGUI::CGradient::ColorStopMap presscsm;
7969         VSTGUI::CGradient *presscg = VSTGUI::CGradient::create(presscsm);
7970         presscg->addColorStop(0, pressbtnbg);
7971 
7972         csz.left = csz.right - buttonwidth;
7973         csz.inset(2, 3);
7974         csz.top--;
7975         csz.bottom += 1;
7976         auto b = new CTextButtonWithHover(csz, this, tag_editor_overlay_close, "X");
7977         b->setVisible(true);
7978         b->setFont(btnFont);
7979         b->setGradient(cg);
7980         b->setFrameColor(btnborder);
7981         b->setTextColor(btntext);
7982         b->setHoverGradient(hovcg);
7983         b->setHoverFrameColor(hovbtnborder);
7984         b->setHoverTextColor(hovbtntext);
7985         b->setGradientHighlighted(presscg);
7986         b->setFrameColorHighlighted(pressbtnborder);
7987         b->setTextColorHighlighted(pressbtntext);
7988         b->setRoundRadius(CCoord(3.f));
7989 
7990         innerc->addView(b);
7991     }
7992 
7993     // add the control inside that in an outline
7994     if (modalOverlay)
7995     {
7996         containerSize = vs;
7997         containerSize.top -= header;
7998         containerSize = containerSize.centerInside(fs);
7999         containerSize.top += header;
8000         containerSize.inset(3, 3);
8001     }
8002     else
8003     {
8004         containerSize = vs.moveTo(0, header).inset(2, 0);
8005         containerSize.bottom -= 2;
8006     }
8007 
8008     c->setViewSize(containerSize);
8009     c->setMouseableArea(containerSize); // sigh
8010     editorOverlayC->addView(c);
8011 
8012     // save the onClose function
8013     editorOverlay.push_back(std::make_pair(editorTag, editorOverlayC));
8014     editorOverlayOnClose[editorOverlayC] = onClose;
8015     editorOverlayContentsWeakReference[editorOverlayC] = c;
8016 }
8017 
getDisplayForTag(long tag)8018 std::string SurgeGUIEditor::getDisplayForTag(long tag)
8019 {
8020     if (tag < start_paramtags)
8021         return "Non-param tag";
8022 
8023     int ptag = tag - start_paramtags;
8024     if ((ptag >= 0) && (ptag < synth->storage.getPatch().param_ptr.size()))
8025     {
8026         Parameter *p = synth->storage.getPatch().param_ptr[ptag];
8027         if (p)
8028         {
8029             char txt[1024];
8030             p->get_display(txt);
8031             return txt;
8032         }
8033     }
8034 
8035     return "Unknown";
8036 }
8037 
promptForMiniEdit(const std::string & value,const std::string & prompt,const std::string & title,const VSTGUI::CPoint & iwhere,std::function<void (const std::string &)> onOK)8038 void SurgeGUIEditor::promptForMiniEdit(const std::string &value, const std::string &prompt,
8039                                        const std::string &title, const VSTGUI::CPoint &iwhere,
8040                                        std::function<void(const std::string &)> onOK)
8041 {
8042     auto fs = CRect(0, 0, getWindowSizeX(), getWindowSizeY());
8043     minieditOverlay = new CViewContainer(fs);
8044     minieditOverlay->setBackgroundColor(currentSkin->getColor(Colors::Overlay::Background));
8045     minieditOverlay->setVisible(true);
8046     frame->addView(minieditOverlay);
8047 
8048     auto where = iwhere;
8049     if (where.x < 0 || where.y < 0)
8050     {
8051         frame->getCurrentMouseLocation(where);
8052         frame->localToFrame(where);
8053     }
8054 
8055     int wd = 160;
8056     int ht = 80;
8057 
8058     // We want to center the text on where. The 0.4 just 'feels better' than 0.5
8059     where = where.offset(-wd * 0.4, -ht * 0.5);
8060 
8061     auto rr = CRect(CPoint(where.x - fs.left, where.y - fs.top), CPoint(wd, ht));
8062 
8063     if (rr.top < 0)
8064         rr = rr.offset(0, 10 - rr.top);
8065     if (rr.bottom > fs.getHeight())
8066 
8067         rr.offset(0, -rr.bottom + fs.getHeight() - 10);
8068     if (rr.left < 0)
8069         rr = rr.offset(10 - rr.left, 0);
8070     if (rr.right > fs.getWidth())
8071 
8072         rr.offset(-rr.right + fs.getWidth() - 10, 0);
8073 
8074     auto fnt = Surge::GUI::getLatoAtSize(11);
8075     auto fnts = Surge::GUI::getLatoAtSize(9);
8076     auto fntt = Surge::GUI::getLatoAtSize(9, kBoldFace);
8077 
8078     auto window = new CViewContainer(rr);
8079     window->setBackgroundColor(currentSkin->getColor(Colors::Dialog::Border));
8080     window->setVisible(true);
8081     minieditOverlay->addView(window);
8082 
8083     auto titlebar = new CViewContainer(CRect(0, 0, wd, 18));
8084     titlebar->setBackgroundColor(currentSkin->getColor(Colors::Dialog::Titlebar::Background));
8085     titlebar->setVisible(true);
8086     window->addView(titlebar);
8087 
8088     auto titlerect = CRect(CPoint(0, 0), CPoint(wd, 16));
8089     auto titletxt = new CTextLabel(titlerect, title.c_str());
8090     titletxt->setTransparency(true);
8091     titletxt->setFontColor(currentSkin->getColor(Colors::Dialog::Titlebar::Text));
8092     titletxt->setFont(fntt);
8093     titlebar->addView(titletxt);
8094 
8095     auto iconrect = CRect(CPoint(3, 2), CPoint(15, 15));
8096     auto icon = new CViewContainer(iconrect);
8097     icon->setBackground(bitmapStore->getBitmap(IDB_SURGE_ICON));
8098     icon->setVisible(true);
8099     titlebar->addView(icon);
8100 
8101     auto bgrect = CRect(CPoint(1, 18), CPoint(wd - 2, ht - 19));
8102     auto bg = new CViewContainer(bgrect);
8103     bg->setBackgroundColor(currentSkin->getColor(Colors::Dialog::Background));
8104     bg->setVisible(true);
8105     window->addView(bg);
8106 
8107     auto msgrect = CRect(CPoint(0, 2), CPoint(wd, 14));
8108     msgrect.inset(5, 0);
8109     auto msgtxt = new CTextLabel(msgrect, prompt.c_str());
8110     msgtxt->setTransparency(true);
8111     msgtxt->setFontColor(currentSkin->getColor(Colors::Dialog::Label::Text));
8112     msgtxt->setFont(fnts);
8113     msgtxt->setHoriAlign(kLeftText);
8114     bg->addView(msgtxt);
8115 
8116     auto mer = CRect(CPoint(0, 18), CPoint(wd - 2, 22));
8117     mer.inset(4, 2);
8118     minieditTypein = new CTextEdit(mer, this, tag_miniedit_ok, value.c_str());
8119     minieditTypein->setBackColor(currentSkin->getColor(Colors::Dialog::Entry::Background));
8120     minieditTypein->setFrameColor(currentSkin->getColor(Colors::Dialog::Entry::Border));
8121     minieditTypein->setFontColor(currentSkin->getColor(Colors::Dialog::Entry::Text));
8122     minieditTypein->setFont(fnt);
8123 #if WINDOWS
8124     minieditTypein->setTextInset(CPoint(3, 0));
8125 #endif
8126     bg->addView(minieditTypein);
8127     minieditTypein->takeFocus();
8128 
8129     minieditOverlayDone = onOK;
8130 
8131     int bw = 44;
8132     auto b1r = CRect(CPoint(wd - (bw * 2) - 9, bgrect.bottom - 36), CPoint(bw, 13));
8133     auto b2r = CRect(CPoint(wd - bw - 6, bgrect.bottom - 36), CPoint(bw, 13));
8134 
8135     auto btnbg = currentSkin->getColor(Colors::Dialog::Button::Background);
8136     auto btnborder = currentSkin->getColor(Colors::Dialog::Button::Border);
8137     auto btntext = currentSkin->getColor(Colors::Dialog::Button::Text);
8138 
8139     auto hovbtnbg = currentSkin->getColor(Colors::Dialog::Button::BackgroundHover);
8140     auto hovbtnborder = currentSkin->getColor(Colors::Dialog::Button::BorderHover);
8141     auto hovbtntext = currentSkin->getColor(Colors::Dialog::Button::TextHover);
8142 
8143     auto pressbtnbg = currentSkin->getColor(Colors::Dialog::Button::BackgroundPressed);
8144     auto pressbtnborder = currentSkin->getColor(Colors::Dialog::Button::BorderPressed);
8145     auto pressbtntext = currentSkin->getColor(Colors::Dialog::Button::TextPressed);
8146 
8147     VSTGUI::CGradient::ColorStopMap csm;
8148     VSTGUI::CGradient *cg = VSTGUI::CGradient::create(csm);
8149     cg->addColorStop(0, btnbg);
8150 
8151     VSTGUI::CGradient::ColorStopMap hovcsm;
8152     VSTGUI::CGradient *hovcg = VSTGUI::CGradient::create(hovcsm);
8153     hovcg->addColorStop(0, hovbtnbg);
8154 
8155     VSTGUI::CGradient::ColorStopMap presscsm;
8156     VSTGUI::CGradient *presscg = VSTGUI::CGradient::create(presscsm);
8157     presscg->addColorStop(0, pressbtnbg);
8158 
8159     auto cb = new CTextButtonWithHover(b1r, this, tag_miniedit_cancel, "Cancel");
8160     cb->setVisible(true);
8161     cb->setFont(fnts);
8162     cb->setGradient(cg);
8163     cb->setFrameColor(btnborder);
8164     cb->setTextColor(btntext);
8165     cb->setHoverGradient(hovcg);
8166     cb->setHoverFrameColor(hovbtnborder);
8167     cb->setHoverTextColor(hovbtntext);
8168     cb->setGradientHighlighted(presscg);
8169     cb->setFrameColorHighlighted(pressbtnborder);
8170     cb->setTextColorHighlighted(pressbtntext);
8171     cb->setRoundRadius(CCoord(3.f));
8172     bg->addView(cb);
8173 
8174     auto kb = new CTextButtonWithHover(b2r, this, tag_miniedit_ok, "OK");
8175     kb->setVisible(true);
8176     kb->setFont(fnts);
8177     kb->setGradient(cg);
8178     kb->setFrameColor(btnborder);
8179     kb->setTextColor(btntext);
8180     kb->setHoverGradient(hovcg);
8181     kb->setHoverFrameColor(hovbtnborder);
8182     kb->setHoverTextColor(hovbtntext);
8183     kb->setGradientHighlighted(presscg);
8184     kb->setFrameColorHighlighted(pressbtnborder);
8185     kb->setTextColorHighlighted(pressbtntext);
8186     kb->setRoundRadius(CCoord(3.f));
8187     bg->addView(kb);
8188 }
8189 
swapControllers(int t1,int t2)8190 void SurgeGUIEditor::swapControllers(int t1, int t2)
8191 {
8192     synth->swapMetaControllers(t1 - tag_mod_source0 - ms_ctrl1, t2 - tag_mod_source0 - ms_ctrl1);
8193 }
8194 
openModTypeinOnDrop(int modt,CControl * sl,int slidertag)8195 void SurgeGUIEditor::openModTypeinOnDrop(int modt, CControl *sl, int slidertag)
8196 {
8197     auto p = synth->storage.getPatch().param_ptr[slidertag - start_paramtags];
8198     int ms = modt - tag_mod_source0;
8199 
8200     if (synth->isValidModulation(p->id, (modsources)ms))
8201         promptForUserValueEntry(p, sl, ms);
8202 }
8203 
resetSmoothing(ControllerModulationSource::SmoothingMode t)8204 void SurgeGUIEditor::resetSmoothing(ControllerModulationSource::SmoothingMode t)
8205 {
8206     // Reset the default value and tell the synth it is updated
8207     Surge::Storage::updateUserDefaultValue(&(synth->storage), "smoothingMode", (int)t);
8208     synth->changeModulatorSmoothing(t);
8209 }
8210 
resetPitchSmoothing(ControllerModulationSource::SmoothingMode t)8211 void SurgeGUIEditor::resetPitchSmoothing(ControllerModulationSource::SmoothingMode t)
8212 {
8213     // Reset the default value and update it in storage for newly created voices to use
8214     Surge::Storage::updateUserDefaultValue(&(synth->storage), "pitchSmoothingMode", (int)t);
8215     synth->storage.pitchSmoothingMode = t;
8216 }
8217 
makeStorePatchDialog()8218 void SurgeGUIEditor::makeStorePatchDialog()
8219 {
8220     auto npc = Surge::Skin::Connector::NonParameterConnection::STORE_PATCH_DIALOG;
8221     auto conn = Surge::Skin::Connector::connectorByNonParameterConnection(npc);
8222     auto skinCtrl = currentSkin->getOrCreateControlForConnector(conn);
8223 
8224     // TODO: add skin connectors for all Store Patch dialog widgets
8225     // let's have fixed dialog size for now, once TODO is done use the commented out line instead
8226     CRect dialogSize(CPoint(0, 0), CPoint(390, 143));
8227     // CRect dialogSize(CPoint(0, 0), CPoint(skinCtrl->w, skinCtrl->h));
8228 
8229     auto saveDialog = new CViewContainer(dialogSize);
8230     saveDialog->setBackgroundColor(currentSkin->getColor(Colors::Dialog::Background));
8231 
8232     auto btnbg = currentSkin->getColor(Colors::Dialog::Button::Background);
8233     auto btnborder = currentSkin->getColor(Colors::Dialog::Button::Border);
8234     auto btntext = currentSkin->getColor(Colors::Dialog::Button::Text);
8235 
8236     auto hovbtnbg = currentSkin->getColor(Colors::Dialog::Button::BackgroundHover);
8237     auto hovbtnborder = currentSkin->getColor(Colors::Dialog::Button::BorderHover);
8238     auto hovbtntext = currentSkin->getColor(Colors::Dialog::Button::TextHover);
8239 
8240     auto pressbtnbg = currentSkin->getColor(Colors::Dialog::Button::BackgroundPressed);
8241     auto pressbtnborder = currentSkin->getColor(Colors::Dialog::Button::BorderPressed);
8242     auto pressbtntext = currentSkin->getColor(Colors::Dialog::Button::TextPressed);
8243 
8244     VSTGUI::CGradient::ColorStopMap csm;
8245     VSTGUI::CGradient *cg = VSTGUI::CGradient::create(csm);
8246     cg->addColorStop(0, btnbg);
8247 
8248     VSTGUI::CGradient::ColorStopMap hovcsm;
8249     VSTGUI::CGradient *hovcg = VSTGUI::CGradient::create(hovcsm);
8250     hovcg->addColorStop(0, hovbtnbg);
8251 
8252     VSTGUI::CGradient::ColorStopMap presscsm;
8253     VSTGUI::CGradient *presscg = VSTGUI::CGradient::create(presscsm);
8254     presscg->addColorStop(0, pressbtnbg);
8255 
8256     auto fnt = Surge::GUI::getLatoAtSize(11);
8257 
8258     auto label = CRect(CPoint(10, 10), CPoint(47, 19));
8259     auto pnamelbl = new CTextLabel(label, "Name");
8260     pnamelbl->setTransparency(true);
8261     pnamelbl->setFont(fnt);
8262     pnamelbl->setFontColor(currentSkin->getColor(Colors::Dialog::Label::Text));
8263     pnamelbl->setHoriAlign(kRightText);
8264 
8265     label.offset(0, 24);
8266     auto pcatlbl = new CTextLabel(label, "Category");
8267     pcatlbl->setTransparency(true);
8268     pcatlbl->setFont(fnt);
8269     pcatlbl->setFontColor(currentSkin->getColor(Colors::Dialog::Label::Text));
8270     pcatlbl->setHoriAlign(kRightText);
8271 
8272     label.offset(0, 24);
8273     auto pauthlbl = new CTextLabel(label, "Author");
8274     pauthlbl->setTransparency(true);
8275     pauthlbl->setFont(fnt);
8276     pauthlbl->setFontColor(currentSkin->getColor(Colors::Dialog::Label::Text));
8277     pauthlbl->setHoriAlign(kRightText);
8278 
8279     label.offset(0, 24);
8280     auto pcomlbl = new CTextLabel(label, "Comment");
8281     pcomlbl->setTransparency(true);
8282     pcomlbl->setFont(fnt);
8283     pcomlbl->setFontColor(currentSkin->getColor(Colors::Dialog::Label::Text));
8284     pcomlbl->setHoriAlign(kRightText);
8285 
8286     patchName = new CTextEdit(CRect(CPoint(67, 10), CPoint(309, 19)), this, tag_store_name);
8287     patchCategory = new CTextEdit(CRect(CPoint(67, 34), CPoint(309, 19)), this, tag_store_category);
8288     patchCreator = new CTextEdit(CRect(CPoint(67, 58), CPoint(309, 19)), this, tag_store_creator);
8289     patchComment = new CTextEdit(CRect(CPoint(67, 82), CPoint(309, 19)), this, tag_store_comments);
8290     patchTuning = new CCheckBox(CRect(CPoint(67, 111), CPoint(200, 20)), this, tag_store_tuning,
8291                                 "Save With Tuning");
8292     patchTuning->setFont(fnt);
8293     patchTuning->setFontColor(currentSkin->getColor(Colors::Dialog::Label::Text));
8294     patchTuning->setBoxFrameColor(currentSkin->getColor(Colors::Dialog::Checkbox::Border));
8295     patchTuning->setBoxFillColor(currentSkin->getColor(Colors::Dialog::Checkbox::Background));
8296     patchTuning->setCheckMarkColor(currentSkin->getColor(Colors::Dialog::Checkbox::Tick));
8297     patchTuning->sizeToFit();
8298     patchTuning->setValue(0);
8299     patchTuning->setMouseEnabled(true);
8300     patchTuning->setVisible(!synth->storage.isStandardTuning);
8301 
8302     // fix the text selection rectangle background overhanging the borders on Windows
8303 #if WINDOWS
8304     patchName->setTextInset(CPoint(3, 0));
8305     patchCategory->setTextInset(CPoint(3, 0));
8306     patchCreator->setTextInset(CPoint(3, 0));
8307     patchComment->setTextInset(CPoint(3, 0));
8308 #endif
8309 
8310     /*
8311      * There is, apparently, a bug in VSTGui that focus events don't fire reliably on some mac
8312      * hosts. This leads to the odd behaviour when you click out of a box that in some hosts - Logic
8313      * Pro for instance - there is no looseFocus event and so the value doesn't update. We could fix
8314      * that a variety of ways I imagine, but since we don't really mind the value being updated as
8315      * we go, we can just set the editors to immediate and correct the problem.
8316      *
8317      * See GitHub Issue #231 for an explanation of the behaviour without these changes as of Jan
8318      * 2019.
8319      */
8320     patchName->setImmediateTextChange(true);
8321     patchCategory->setImmediateTextChange(true);
8322     patchCreator->setImmediateTextChange(true);
8323     patchComment->setImmediateTextChange(true);
8324 
8325     patchName->setBackColor(currentSkin->getColor(Colors::Dialog::Entry::Background));
8326     patchCategory->setBackColor(currentSkin->getColor(Colors::Dialog::Entry::Background));
8327     patchCreator->setBackColor(currentSkin->getColor(Colors::Dialog::Entry::Background));
8328     patchComment->setBackColor(currentSkin->getColor(Colors::Dialog::Entry::Background));
8329 
8330     patchName->setFontColor(currentSkin->getColor(Colors::Dialog::Entry::Text));
8331     patchCategory->setFontColor(currentSkin->getColor(Colors::Dialog::Entry::Text));
8332     patchCreator->setFontColor(currentSkin->getColor(Colors::Dialog::Entry::Text));
8333     patchComment->setFontColor(currentSkin->getColor(Colors::Dialog::Entry::Text));
8334 
8335     patchName->setFrameColor(currentSkin->getColor(Colors::Dialog::Entry::Border));
8336     patchCategory->setFrameColor(currentSkin->getColor(Colors::Dialog::Entry::Border));
8337     patchCreator->setFrameColor(currentSkin->getColor(Colors::Dialog::Entry::Border));
8338     patchComment->setFrameColor(currentSkin->getColor(Colors::Dialog::Entry::Border));
8339 
8340     auto b1r = CRect(CPoint(266, 111), CPoint(50, 20));
8341     auto cb = new CTextButtonWithHover(b1r, this, tag_store_cancel, "Cancel");
8342     cb->setFont(aboutFont);
8343     cb->setGradient(cg);
8344     cb->setFrameColor(btnborder);
8345     cb->setTextColor(btntext);
8346     cb->setHoverGradient(hovcg);
8347     cb->setHoverFrameColor(hovbtnborder);
8348     cb->setHoverTextColor(hovbtntext);
8349     cb->setGradientHighlighted(presscg);
8350     cb->setFrameColorHighlighted(pressbtnborder);
8351     cb->setTextColorHighlighted(pressbtntext);
8352     cb->setRoundRadius(CCoord(3.f));
8353 
8354     auto b2r = CRect(CPoint(326, 111), CPoint(50, 20));
8355     auto kb = new CTextButtonWithHover(b2r, this, tag_store_ok, "OK");
8356     kb->setFont(aboutFont);
8357     kb->setGradient(cg);
8358     kb->setFrameColor(btnborder);
8359     kb->setTextColor(btntext);
8360     kb->setHoverGradient(hovcg);
8361     kb->setHoverFrameColor(hovbtnborder);
8362     kb->setHoverTextColor(hovbtntext);
8363     kb->setGradientHighlighted(presscg);
8364     kb->setFrameColorHighlighted(pressbtnborder);
8365     kb->setTextColorHighlighted(pressbtntext);
8366     kb->setRoundRadius(CCoord(3.f));
8367 
8368     saveDialog->addView(pnamelbl);
8369     saveDialog->addView(pcatlbl);
8370     saveDialog->addView(pauthlbl);
8371     saveDialog->addView(pcomlbl);
8372     saveDialog->addView(patchName);
8373     saveDialog->addView(patchCategory);
8374     saveDialog->addView(patchCreator);
8375     saveDialog->addView(patchComment);
8376     saveDialog->addView(patchTuning);
8377     saveDialog->addView(patchTuningLabel);
8378     saveDialog->addView(cb);
8379     saveDialog->addView(kb);
8380 
8381     addEditorOverlay(saveDialog, "Store Patch", STORE_PATCH, CPoint(skinCtrl->x, skinCtrl->y),
8382                      false, false, [this]() {});
8383 }
8384 
8385 VSTGUI::CControl *
layoutComponentForSkin(std::shared_ptr<Surge::UI::Skin::Control> skinCtrl,long tag,int paramIndex,Parameter * p,int style)8386 SurgeGUIEditor::layoutComponentForSkin(std::shared_ptr<Surge::UI::Skin::Control> skinCtrl, long tag,
8387                                        int paramIndex, Parameter *p, int style)
8388 {
8389     // Special cases to preserve things
8390     if (p && p->ctrltype == ct_fmconfig)
8391     {
8392         fmconfig_tag = tag;
8393     }
8394     if (p && p->ctrltype == ct_fbconfig)
8395     {
8396         filterblock_tag = tag;
8397     }
8398     if (p && p->ctrltype == ct_fxbypass)
8399     {
8400         fxbypass_tag = tag;
8401     }
8402     // Basically put this in a function
8403     if (skinCtrl->ultimateparentclassname == "CSurgeSlider")
8404     {
8405         if (!p)
8406         {
8407             // FIXME ERROR
8408             return nullptr;
8409         }
8410 
8411         CPoint loc(skinCtrl->x, skinCtrl->y + p->posy_offset * yofs);
8412 
8413         if (p->is_discrete_selection())
8414         {
8415             loc.offset(2, 4);
8416             auto hs = new CMenuAsSlider(loc, this, p->id + start_paramtags, bitmapStore,
8417                                         &(synth->storage));
8418 
8419             auto *parm = dynamic_cast<ParameterDiscreteIndexRemapper *>(p->user_data);
8420             if (parm && parm->supportsTotalIndexOrdering())
8421                 hs->intOrdering = parm->totalIndexOrdering();
8422 
8423             hs->setSkin(currentSkin, bitmapStore);
8424             hs->setValue(p->get_value_f01());
8425             hs->setMinMax(p->val_min.i, p->val_max.i);
8426             hs->setLabel(p->get_name());
8427             p->ctrlstyle = p->ctrlstyle | kNoPopup;
8428             frame->addView(hs);
8429             if (p->can_deactivate())
8430                 hs->setDeactivated(p->deactivated);
8431 
8432             param[paramIndex] = hs;
8433             return hs;
8434         }
8435         else
8436         {
8437             p->ctrlstyle = p->ctrlstyle & ~kNoPopup;
8438         }
8439         bool is_mod = p && synth->isValidModulation(p->id, modsource);
8440 
8441         auto hs = new CSurgeSlider(loc, style, this, tag, is_mod, bitmapStore, &(synth->storage));
8442         hs->setSkin(currentSkin, bitmapStore, skinCtrl);
8443         hs->setMoveRate(p->moverate);
8444 
8445         if (p->can_temposync())
8446             hs->setTempoSync(p->temposync);
8447         hs->setValue(p->get_value_f01());
8448 
8449         if (p->supportsDynamicName() && p->dynamicName)
8450         {
8451             hs->setDynamicLabel([p]() { return std::string(p->get_name()); });
8452         }
8453         else
8454         {
8455             hs->setLabel(p->get_name());
8456         }
8457 
8458         hs->setBipolarFunction([p]() { return p->is_bipolar(); });
8459         hs->setModPresent(synth->isModDestUsed(p->id));
8460         hs->setDefaultValue(p->get_default_value_f01());
8461         hs->font_style = Surge::UI::Skin::setFontStyleProperty(
8462             currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::FONT_STYLE, "normal"));
8463 
8464         hs->text_align = Surge::UI::Skin::setTextAlignProperty(
8465             currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::TEXT_ALIGN, "right"));
8466 
8467         // CSurgeSlider is using labfont = displayFont, which is currently 9 pt in size
8468         // TODO: Pull the default font size from some central location at a later date
8469         hs->font_size = std::atoi(
8470             currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::FONT_SIZE, "9").c_str());
8471 
8472         hs->text_hoffset = std::atoi(
8473             currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::TEXT_HOFFSET, "0")
8474                 .c_str());
8475 
8476         hs->text_voffset = std::atoi(
8477             currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::TEXT_VOFFSET, "0")
8478                 .c_str());
8479 
8480         hs->setDeactivatedFn([p]() { return p->appears_deactivated(); });
8481 
8482 #if !TARGET_JUCE_UI && 0
8483         auto ff = currentSkin->propertyValue(skinCtrl, "font-family", "");
8484         if (ff.size() > 0)
8485         {
8486             hs->setFont(new CFontDesc(ff.c_str(), 9));
8487         }
8488 #endif
8489 
8490         if (p->valtype == vt_int || p->valtype == vt_bool)
8491         {
8492             hs->isStepped = true;
8493             hs->intRange = p->val_max.i - p->val_min.i;
8494         }
8495         else
8496         {
8497             hs->isStepped = false;
8498         }
8499 
8500         setDisabledForParameter(p, hs);
8501 
8502         if (synth->isValidModulation(p->id, modsource))
8503         {
8504             hs->setModMode(mod_editor ? 1 : 0);
8505             hs->setModValue(synth->getModulation(p->id, modsource));
8506             hs->setModCurrent(synth->isActiveModulation(p->id, modsource),
8507                               synth->isBipolarModulation(modsource));
8508         }
8509         else
8510         {
8511             // Even if current modsource isn't modulating me, something else may be
8512         }
8513         frame->addView(hs);
8514         if (paramIndex >= 0)
8515             param[paramIndex] = hs;
8516         return hs;
8517     }
8518     if (skinCtrl->ultimateparentclassname == "CHSwitch2")
8519     {
8520         CRect rect(0, 0, skinCtrl->w, skinCtrl->h);
8521         rect.offset(skinCtrl->x, skinCtrl->y);
8522 
8523         // Make this a function on skin
8524         auto bmp = currentSkin->backgroundBitmapForControl(skinCtrl, bitmapStore);
8525 
8526         if (bmp)
8527         {
8528             // Special case that scene select parameter is "odd"
8529             if (p && p->ctrltype == ct_scenesel)
8530                 tag = tag_scene_select;
8531 
8532             auto frames = currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::FRAMES, "1");
8533             auto rows = currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::ROWS, "1");
8534             auto cols = currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::COLUMNS, "1");
8535             auto frameoffset =
8536                 currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::FRAME_OFFSET, "0");
8537             auto drgb = currentSkin->propertyValue(skinCtrl,
8538                                                    Surge::Skin::Component::DRAGGABLE_HSWITCH, "1");
8539             auto hsw = new CHSwitch2(rect, this, tag, std::atoi(frames.c_str()), skinCtrl->h,
8540                                      std::atoi(rows.c_str()), std::atoi(cols.c_str()), bmp,
8541                                      CPoint(0, 0), std::atoi(drgb.c_str()));
8542             if (p)
8543             {
8544                 auto fval = p->get_value_f01();
8545 
8546                 if (p->ctrltype == ct_scenemode)
8547                 {
8548                     /*
8549                     ** SceneMode is special now because we have a streaming vs UI difference.
8550                     ** The streamed integer value is 0, 1, 2, 3 which matches the scene_mode
8551                     ** SurgeStorage enum. But our display would look gross in that order, so
8552                     ** our display order is single, split, channel split, dual which is 0, 1, 3, 2.
8553                     ** Fine. So just deal with that here.
8554                     */
8555                     auto guiscenemode = p->val.i;
8556                     if (guiscenemode == 3)
8557                         guiscenemode = 2;
8558                     else if (guiscenemode == 2)
8559                         guiscenemode = 3;
8560                     fval = Parameter::intScaledToFloat(guiscenemode, n_scene_modes - 1);
8561                 }
8562                 hsw->setValue(fval);
8563             }
8564             hsw->setSkin(currentSkin, bitmapStore, skinCtrl);
8565             hsw->setMouseableArea(rect);
8566             hsw->frameOffset = std::atoi(frameoffset.c_str());
8567 
8568             frame->addView(hsw);
8569             if (paramIndex >= 0)
8570                 nonmod_param[paramIndex] = hsw;
8571 
8572             return hsw;
8573         }
8574         else
8575         {
8576             std::cout << "Can't get a CHSwitch2 BG" << std::endl;
8577         }
8578     }
8579     if (skinCtrl->ultimateparentclassname == "CSwitchControl")
8580     {
8581         CRect rect(CPoint(skinCtrl->x, skinCtrl->y), CPoint(skinCtrl->w, skinCtrl->h));
8582         auto bmp = currentSkin->backgroundBitmapForControl(skinCtrl, bitmapStore);
8583         if (bmp)
8584         {
8585             CSwitchControl *hsw = new CSwitchControl(rect, this, tag, bmp);
8586             hsw->setSkin(currentSkin, bitmapStore, skinCtrl);
8587             hsw->setMouseableArea(rect);
8588             frame->addView(hsw);
8589             if (paramIndex >= 0)
8590                 nonmod_param[paramIndex] = hsw;
8591             if (p)
8592             {
8593                 hsw->setValue(p->get_value_f01());
8594 
8595                 // Carry over this filter type special case from the default control path
8596                 if (p->ctrltype == ct_filtersubtype)
8597                 {
8598                     auto filttype = synth->storage.getPatch()
8599                                         .scene[current_scene]
8600                                         .filterunit[p->ctrlgroup_entry]
8601                                         .type.val.i;
8602                     auto stc = fut_subcount[filttype];
8603                     hsw->is_itype = true;
8604                     hsw->imax = stc;
8605                     hsw->ivalue = std::min(p->val.i + 1, stc);
8606                     if (fut_subcount[filttype] == 0)
8607                         hsw->ivalue = 0;
8608 
8609                     if (p->ctrlgroup_entry == 1)
8610                     {
8611                         f2subtypetag = p->id + start_paramtags;
8612                         filtersubtype[1] = hsw;
8613                     }
8614                     else
8615                     {
8616                         f1subtypetag = p->id + start_paramtags;
8617                         filtersubtype[0] = hsw;
8618                     }
8619                 }
8620             }
8621             return hsw;
8622         }
8623     }
8624     if (skinCtrl->ultimateparentclassname == "CLFOGui")
8625     {
8626         if (!p)
8627             return nullptr;
8628         if (p->ctrltype != ct_lfotype)
8629         {
8630             // FIXME - warning?
8631         }
8632         CRect rect(0, 0, skinCtrl->w, skinCtrl->h);
8633         rect.offset(skinCtrl->x, skinCtrl->y);
8634         int lfo_id = p->ctrlgroup_entry - ms_lfo1;
8635         if ((lfo_id >= 0) && (lfo_id < n_lfos))
8636         {
8637             CLFOGui *slfo = new CLFOGui(
8638                 rect, lfo_id >= 0 && lfo_id <= (ms_lfo6 - ms_lfo1), this, p->id + start_paramtags,
8639                 &synth->storage.getPatch().scene[current_scene].lfo[lfo_id], &synth->storage,
8640                 &synth->storage.getPatch().stepsequences[current_scene][lfo_id],
8641                 &synth->storage.getPatch().msegs[current_scene][lfo_id],
8642                 &synth->storage.getPatch().formulamods[current_scene][lfo_id], bitmapStore);
8643             slfo->setSkin(currentSkin, bitmapStore, skinCtrl);
8644             lfodisplay = slfo;
8645             frame->addView(slfo);
8646             nonmod_param[paramIndex] = slfo;
8647             return slfo;
8648         }
8649     }
8650     if (skinCtrl->ultimateparentclassname == "COSCMenu")
8651     {
8652         CRect rect(0, 0, skinCtrl->w, skinCtrl->h);
8653         rect.offset(skinCtrl->x, skinCtrl->y);
8654         auto hsw = new COscMenu(
8655             rect, this, tag_osc_menu, &synth->storage,
8656             &synth->storage.getPatch().scene[current_scene].osc[current_osc[current_scene]],
8657             bitmapStore);
8658 
8659         hsw->setSkin(currentSkin, bitmapStore);
8660         hsw->setMouseableArea(rect);
8661 
8662         hsw->text_allcaps = Surge::UI::Skin::setAllCapsProperty(
8663             currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::TEXT_ALL_CAPS, "false"));
8664         hsw->font_style = Surge::UI::Skin::setFontStyleProperty(
8665             currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::FONT_STYLE, "normal"));
8666         hsw->text_align = Surge::UI::Skin::setTextAlignProperty(
8667             currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::TEXT_ALIGN, "center"));
8668         hsw->font_size = std::atoi(
8669             currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::FONT_SIZE, "8").c_str());
8670         hsw->text_hoffset = std::atoi(
8671             currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::TEXT_HOFFSET, "0")
8672                 .c_str());
8673         hsw->text_voffset = std::atoi(
8674             currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::TEXT_VOFFSET, "0")
8675                 .c_str());
8676 
8677         if (p)
8678             hsw->setValue(p->get_value_f01());
8679         // TODO: This was not on before skinnification. Why?
8680         // if( paramIndex >= 0 ) nonmod_param[paramIndex] = hsw;
8681 
8682         frame->addView(hsw);
8683         return hsw;
8684     }
8685     if (skinCtrl->ultimateparentclassname == "CFXMenu")
8686     {
8687         CRect rect(0, 0, skinCtrl->w, skinCtrl->h);
8688         rect.offset(skinCtrl->x, skinCtrl->y);
8689         // CControl *m = new
8690         // CFxMenu(rect,this,tag_fx_menu,&synth->storage,&synth->storage.getPatch().fx[current_fx],current_fx);
8691         CControl *m = new CFxMenu(rect, this, tag_fx_menu, &synth->storage,
8692                                   &synth->storage.getPatch().fx[current_fx],
8693                                   &synth->fxsync[current_fx], current_fx);
8694         m->setMouseableArea(rect);
8695         ((CFxMenu *)m)->setSkin(currentSkin, bitmapStore);
8696         ((CFxMenu *)m)->selectedIdx = this->selectedFX[current_fx];
8697         fxPresetLabel->setText(this->fxPresetName[current_fx].c_str());
8698         m->setValue(p->get_value_f01());
8699         frame->addView(m);
8700         fxmenu = m;
8701         return m;
8702     }
8703     if (skinCtrl->ultimateparentclassname == "CNumberField")
8704     {
8705         CRect rect(0, 0, skinCtrl->w, skinCtrl->h);
8706         rect.offset(skinCtrl->x, skinCtrl->y);
8707         CNumberField *pbd = new CNumberField(rect, this, tag, nullptr, &(synth->storage));
8708         pbd->setSkin(currentSkin, bitmapStore, skinCtrl);
8709         pbd->setMouseableArea(rect);
8710 
8711         // TODO extra from properties
8712         auto nfcm = currentSkin->propertyValue(
8713             skinCtrl, Surge::Skin::Component::NUMBERFIELD_CONTROLMODE, std::to_string(cm_none));
8714         pbd->setControlMode(std::atoi(nfcm.c_str()));
8715 
8716         pbd->setValue(p->get_value_f01());
8717         frame->addView(pbd);
8718         nonmod_param[paramIndex] = pbd;
8719 
8720         if (p && p->ctrltype == ct_midikey_or_channel)
8721         {
8722             auto sm = this->synth->storage.getPatch().scenemode.val.i;
8723 
8724             switch (sm)
8725             {
8726             case sm_single:
8727             case sm_dual:
8728                 pbd->setControlMode(cm_none);
8729                 break;
8730             case sm_split:
8731                 pbd->setControlMode(cm_notename);
8732                 break;
8733             case sm_chsplit:
8734                 pbd->setControlMode(cm_midichannel_from_127);
8735                 break;
8736             }
8737         }
8738 
8739         // Save some of these for later reference
8740         switch (p->ctrltype)
8741         {
8742         case ct_polylimit:
8743             polydisp = pbd;
8744             break;
8745         case ct_midikey_or_channel:
8746             splitpointControl = pbd;
8747             break;
8748         default:
8749             break;
8750         }
8751         return pbd;
8752     }
8753     if (skinCtrl->ultimateparentclassname == "FilterSelector")
8754     {
8755         // Obviously exposing this widget as a controllable widget would be better
8756         if (!p)
8757             return nullptr;
8758 
8759         auto rect = skinCtrl->getRect();
8760         auto hsw = new CMenuAsSlider(rect.getTopLeft(), rect.getSize(), this,
8761                                      p->id + start_paramtags, bitmapStore, &(synth->storage));
8762 
8763         auto *parm = dynamic_cast<ParameterDiscreteIndexRemapper *>(p->user_data);
8764         if (parm && parm->supportsTotalIndexOrdering())
8765             hsw->intOrdering = parm->totalIndexOrdering();
8766 
8767         hsw->setMinMax(0, n_fu_types - 1);
8768         hsw->setMouseableArea(rect);
8769         hsw->setLabel(p->get_name());
8770         hsw->setDeactivated(false);
8771         hsw->setBackgroundID(IDB_FILTER_MENU);
8772 
8773         bool activeGlyph = true;
8774         if (currentSkin->getVersion() >= 2)
8775         {
8776             auto pv =
8777                 currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::GLPYH_ACTIVE, "true");
8778             if (pv == "false")
8779                 activeGlyph = false;
8780         }
8781 
8782         hsw->setFilterMode(true);
8783 
8784         if (activeGlyph)
8785         {
8786             for (int i = 0; i < n_fu_types; i++)
8787                 hsw->glyphIndexMap.push_back(
8788                     std::make_pair(fut_glyph_index[i][0], fut_glyph_index[i][1]));
8789             if (currentSkin->getVersion() == 1)
8790             {
8791                 auto drr = rect;
8792                 drr.right = drr.left + 18;
8793                 hsw->setDragRegion(drr);
8794                 hsw->setDragGlyph(IDB_FILTER_ICONS, 18);
8795             }
8796         }
8797 
8798         p->ctrlstyle = p->ctrlstyle | kNoPopup;
8799 
8800         hsw->setValue(p->get_value_f01());
8801         hsw->setSkin(currentSkin, bitmapStore, skinCtrl);
8802 
8803         if (currentSkin->getVersion() > 1)
8804         {
8805             if (activeGlyph)
8806             {
8807                 auto glpc = currentSkin->propertyValue(
8808                     skinCtrl, Surge::Skin::Component::GLYPH_PLACEMENT, "left");
8809                 auto glw = std::atoi(
8810                     currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::GLYPH_W, "18")
8811                         .c_str());
8812                 auto glh = std::atoi(
8813                     currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::GLYPH_H, "18")
8814                         .c_str());
8815                 auto gli =
8816                     currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::GLYPH_IMAGE, "");
8817                 auto glih = currentSkin->propertyValue(
8818                     skinCtrl, Surge::Skin::Component::GLYPH_HOVER_IMAGE, "");
8819 
8820                 // These are the V1 hardcoded defaults
8821                 if (glw == 18 && glh == 18 && glpc == "left" && gli == "")
8822                 {
8823                     auto drr = rect;
8824                     drr.right = drr.left + 18;
8825                     hsw->setDragRegion(drr);
8826                     hsw->setDragGlyph(IDB_FILTER_ICONS, 18);
8827                 }
8828                 else
8829                 {
8830                     hsw->setGlyphSettings(gli, glih, glpc, glw, glh);
8831                 }
8832             }
8833 
8834             auto pv = currentSkin->propertyValue(skinCtrl, Surge::Skin::Component::BACKGROUND);
8835             if (pv.isJust())
8836             {
8837                 hsw->setBackgroundBitmapResource(pv.fromJust());
8838             }
8839         }
8840 
8841         frame->addView(hsw);
8842         nonmod_param[paramIndex] = hsw;
8843         return hsw;
8844     }
8845     if (skinCtrl->ultimateparentclassname != Surge::UI::NoneClassName)
8846         std::cout << "Unable to make control with upc " << skinCtrl->ultimateparentclassname
8847                   << std::endl;
8848     return nullptr;
8849 }
8850 
canDropTarget(const std::string & fname)8851 bool SurgeGUIEditor::canDropTarget(const std::string &fname)
8852 {
8853     static std::unordered_set<std::string> extensions;
8854     if (extensions.empty())
8855     {
8856         extensions.insert(".scl");
8857         extensions.insert(".kbm");
8858         extensions.insert(".wav");
8859         extensions.insert(".wt");
8860         extensions.insert(".fxp");
8861     }
8862 
8863     fs::path fPath(fname);
8864     std::string fExt(path_to_string(fPath.extension()));
8865     std::transform(fExt.begin(), fExt.end(), fExt.begin(),
8866                    [](unsigned char c) { return std::tolower(c); });
8867     if (extensions.find(fExt) != extensions.end())
8868         return true;
8869     return false;
8870 }
onDrop(const std::string & fname)8871 bool SurgeGUIEditor::onDrop(const std::string &fname)
8872 {
8873     fs::path fPath(fname);
8874     std::string fExt(path_to_string(fPath.extension()));
8875     std::transform(fExt.begin(), fExt.end(), fExt.begin(),
8876                    [](unsigned char c) { return std::tolower(c); });
8877     if (fExt == ".wav" || fExt == ".wt")
8878     {
8879         strxcpy(synth->storage.getPatch()
8880                     .scene[current_scene]
8881                     .osc[current_osc[current_scene]]
8882                     .wt.queue_filename,
8883                 fname.c_str(), 255);
8884     }
8885     else if (fExt == ".scl")
8886     {
8887         scaleFileDropped(fname);
8888     }
8889     else if (fExt == ".kbm")
8890     {
8891         mappingFileDropped(fname);
8892     }
8893     else if (fExt == ".fxp")
8894     {
8895         queuePatchFileLoad(fname);
8896     }
8897 
8898     return true;
8899 }
8900 
swapFX(int source,int target,SurgeSynthesizer::FXReorderMode m)8901 void SurgeGUIEditor::swapFX(int source, int target, SurgeSynthesizer::FXReorderMode m)
8902 {
8903     if (source < 0 || source >= n_fx_slots || target < 0 || target >= n_fx_slots)
8904         return;
8905 
8906     auto t = fxPresetName[target];
8907     fxPresetName[target] = fxPresetName[source];
8908     if (m == SurgeSynthesizer::SWAP)
8909         fxPresetName[source] = t;
8910     if (m == SurgeSynthesizer::MOVE)
8911         fxPresetName[source] = "";
8912 
8913     synth->reorderFx(source, target, m);
8914 }
8915 
lfoShapeChanged(int prior,int curr)8916 void SurgeGUIEditor::lfoShapeChanged(int prior, int curr)
8917 {
8918     if (prior != curr || prior == lt_mseg || curr == lt_mseg)
8919     {
8920         if (msegEditSwitch)
8921         {
8922             msegEditSwitch->setVisible(curr == lt_mseg);
8923         }
8924     }
8925 
8926     if (curr == lt_mseg && isAnyOverlayPresent(MSEG_EDITOR))
8927     {
8928         // We have the MSEGEditor open and have swapped to the MSEG here
8929         showMSEGEditor();
8930     }
8931     else if (prior == lt_mseg && curr != lt_mseg && isAnyOverlayPresent(MSEG_EDITOR))
8932     {
8933         // We can choose to not do this too; if we do we are editing an MSEG which isn't used though
8934         closeMSEGEditor();
8935     }
8936 
8937     // update the LFO title label
8938     std::string modname = modulatorName(modsource_editor[current_scene], true);
8939     lfoNameLabel->setText(modname.c_str());
8940     lfoNameLabel->invalid();
8941 
8942     // And now we have dynamic labels really anything
8943     frame->invalid();
8944 }
8945 
closeStorePatchDialog()8946 void SurgeGUIEditor::closeStorePatchDialog()
8947 {
8948     dismissEditorOfType(STORE_PATCH);
8949 
8950     // Have to update all that state too for the newly orphaned items
8951     patchName = nullptr;
8952     patchCategory = nullptr;
8953     patchCreator = nullptr;
8954     patchComment = nullptr;
8955 }
8956 
showStorePatchDialog()8957 void SurgeGUIEditor::showStorePatchDialog() { makeStorePatchDialog(); }
8958 
closeMSEGEditor()8959 void SurgeGUIEditor::closeMSEGEditor()
8960 {
8961     if (isAnyOverlayPresent(MSEG_EDITOR))
8962     {
8963         broadcastMSEGState();
8964         dismissEditorOfType(MSEG_EDITOR);
8965     }
8966 }
toggleMSEGEditor()8967 void SurgeGUIEditor::toggleMSEGEditor()
8968 {
8969     if (isAnyOverlayPresent(MSEG_EDITOR))
8970     {
8971         closeMSEGEditor();
8972     }
8973     else
8974     {
8975         showMSEGEditor();
8976     }
8977 }
8978 
8979 /*
8980  * The edit state is independent per LFO. We want to sync some of ti as if it is not
8981  * so this is called at the appropriate time.
8982  */
broadcastMSEGState()8983 void SurgeGUIEditor::broadcastMSEGState()
8984 {
8985     if (msegIsOpenFor >= 0 && msegIsOpenInScene >= 0)
8986     {
8987         for (int s = 0; s < n_scenes; ++s)
8988         {
8989             for (int lf = 0; lf < n_lfos; ++lf)
8990             {
8991                 msegEditState[s][lf].timeEditMode =
8992                     msegEditState[msegIsOpenInScene][msegIsOpenFor].timeEditMode;
8993             }
8994         }
8995     }
8996     msegIsOpenFor = -1;
8997     msegIsOpenInScene = -1;
8998 }
showMSEGEditor()8999 void SurgeGUIEditor::showMSEGEditor()
9000 {
9001     broadcastMSEGState();
9002 
9003     auto lfo_id = modsource_editor[current_scene] - ms_lfo1;
9004     msegIsOpenFor = lfo_id;
9005     msegIsOpenInScene = current_scene;
9006 
9007     auto lfodata = &synth->storage.getPatch().scene[current_scene].lfo[lfo_id];
9008     auto ms = &synth->storage.getPatch().msegs[current_scene][lfo_id];
9009     auto mse = new MSEGEditor(&(synth->storage), lfodata, ms, &msegEditState[current_scene][lfo_id],
9010                               currentSkin, bitmapStore);
9011     auto vs = mse->getViewSize().getWidth();
9012     float xp = (currentSkin->getWindowSizeX() - (vs + 8)) * 0.5;
9013 
9014     std::string title = modsource_names[modsource_editor[current_scene]];
9015     title += " Editor";
9016     Surge::Storage::findReplaceSubstring(title, std::string("LFO"), std::string("MSEG"));
9017 
9018     auto npc = Surge::Skin::Connector::NonParameterConnection::MSEG_EDITOR_WINDOW;
9019     auto conn = Surge::Skin::Connector::connectorByNonParameterConnection(npc);
9020     auto skinCtrl = currentSkin->getOrCreateControlForConnector(conn);
9021 
9022     addEditorOverlay(mse, title, MSEG_EDITOR, CPoint(skinCtrl->x, skinCtrl->y), false, true,
9023                      [this]() {
9024                          if (msegEditSwitch)
9025                          {
9026                              msegEditSwitch->setValue(0.0);
9027                              msegEditSwitch->invalid();
9028                          }
9029                      });
9030 
9031     if (msegEditSwitch)
9032     {
9033         msegEditSwitch->setValue(1.0);
9034         msegEditSwitch->invalid();
9035     }
9036 }
9037 
repushAutomationFor(Parameter * p)9038 void SurgeGUIEditor::repushAutomationFor(Parameter *p)
9039 {
9040     auto id = synth->idForParameter(p);
9041     synth->sendParameterAutomation(id, synth->getParameter01(id));
9042 
9043 #if TARGET_AUDIOUNIT
9044     synth->getParent()->ParameterBeginEdit(id.getDawSideIndex());
9045     synth->getParent()->ParameterUpdate(id.getDawSideIndex());
9046     synth->getParent()->ParameterEndEdit(id.getDawSideIndex());
9047 #endif
9048 }
9049 
showAboutBox(int devModeGrid)9050 void SurgeGUIEditor::showAboutBox(int devModeGrid)
9051 {
9052     CRect wsize(0, 0, getWindowSizeX(), getWindowSizeY());
9053     aboutbox = new CAboutBox(wsize, this, &(synth->storage), synth->hostProgram, currentSkin,
9054                              bitmapStore, devModeGrid);
9055     aboutbox->setVisible(true);
9056     getFrame()->addView(aboutbox);
9057 }
9058 
hideAboutBox()9059 void SurgeGUIEditor::hideAboutBox()
9060 {
9061     if (aboutbox)
9062     {
9063         aboutbox->setVisible(false);
9064         removeFromFrame.push_back(aboutbox);
9065         aboutbox = nullptr;
9066     }
9067 }
9068 
showMidiLearnOverlay(const VSTGUI::CRect & r)9069 void SurgeGUIEditor::showMidiLearnOverlay(const VSTGUI::CRect &r)
9070 {
9071     if (midiLearnOverlay)
9072         hideMidiLearnOverlay();
9073 
9074     midiLearnOverlay = new CMidiLearnOverlay(r, bitmapStore);
9075     midiLearnOverlay->setVisible(true);
9076     getFrame()->addView(midiLearnOverlay);
9077 }
9078 
hideMidiLearnOverlay()9079 void SurgeGUIEditor::hideMidiLearnOverlay()
9080 {
9081     if (midiLearnOverlay)
9082     {
9083         midiLearnOverlay->setVisible(false);
9084         removeFromFrame.push_back(midiLearnOverlay);
9085         midiLearnOverlay = nullptr;
9086     }
9087 }
9088 
toggleAlternateFor(VSTGUI::CControl * c)9089 void SurgeGUIEditor::toggleAlternateFor(VSTGUI::CControl *c)
9090 {
9091     auto cms = dynamic_cast<CModulationSourceButton *>(c);
9092     if (cms)
9093     {
9094         modsource = (modsources)(cms->getTag() - tag_mod_source0);
9095         cms->setUseAlternate(!cms->useAlternate);
9096         modsource_is_alternate[modsource] = cms->useAlternate;
9097         this->refresh_mod();
9098     }
9099 }
9100